如何在Angular中Mock CdkVirtualScrollViewport进行无限滚动测试?
如何在Angular中Mock CdkVirtualScrollViewport以实现无限滚动测试?
组件TS代码
@ViewChild('scroller') scroller!: CdkVirtualScrollViewport; constructor(private ngZone: NgZone) { } ngAfterViewInit(): void { this.unsub = this.scroller.elementScrolled().pipe( map(() => this.scroller.measureScrollOffset('bottom')), pairwise(), filter(([y1, y2]) => (y2 < y1 && y2 < 140)), throttleTime(200) ).subscribe(() => { this.ngZone.run(() => { (this.maxItems > this.listItems.length) && this.fetchMore(); }); }) }
组件HTML代码
<cdk-virtual-scroll-viewport class="example-viewport" #scroller itemSize="72"> <table mat-table [dataSource]="dataSource" class="mat-elevation-z8"> <ng-container matColumnDef="name"> <th mat-header-cell *matHeaderCellDef> Name </th> <td mat-cell *matCellDef="let element"> {{element.name}} </td> </ng-container> <ng-container matColumnDef="symbol"> <th mat-header-cell *matHeaderCellDef> Symbol </th> <td mat-cell *matCellDef="let element"> {{element.symbol}} </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </table> </cdk-virtual-scroll-viewport>
当前测试用例
it('should check \'ngAfterViewInit\'', () => { component.ngAfterViewInit(); changeDetectorRef.detectChanges(); spyOn(virtualScrollViewport, 'elementScrolled').and.callThrough(); expect(virtualScrollViewport.elementScrolled).toHaveBeenCalled(); });
Mock时出现的错误栈
Error: Error: cdk-virtual-scroll-viewport requires the "itemSize" property to be set. at new CdkVirtualScrollViewport (http://localhost:9876/_karma_webpack_/node_modules/@angular/cdk/__ivy_ngcc__/fesm2015/scrolling.js:1264:1) at new MockCdkVirtualScrollViewport (http://localhost:9876/_karma_webpack_/main.js:27773:9) at Object.factory (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:17371:1) at R3Injector.hydrate (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:17247:42) at R3Injector.get (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:16997:1) at NgModuleRef$1.get (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:36383:1) at TestBedRender3.inject (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/testing.js:3227:1) at Function.inject (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/__ivy_ngcc__/fesm2015/testing.js:3110:1) at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/app/components/gridview/gridview.component.spec.ts:112:37)
解决方案
1. 正确Mock CdkVirtualScrollViewport
错误根源是Mock时未提供itemSize属性,CdkVirtualScrollViewport初始化必须依赖该属性。需要手动编写Mock类替代自动生成的Mock:
import { Subject } from 'rxjs'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; class MockCdkVirtualScrollViewport extends CdkVirtualScrollViewport { private elementScrolledSubject = new Subject<void>(); // 必须设置itemSize以通过初始化校验 itemSize = 72; elementScrolled() { return this.elementScrolledSubject.asObservable(); } measureScrollOffset(position: 'bottom') { // 可根据测试场景返回不同偏移值 return 100; } // 手动触发滚动事件的方法,供测试调用 triggerScroll() { this.elementScrolledSubject.next(); } }
在TestBed中配置注入:
beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [YourComponent], providers: [ { provide: CdkVirtualScrollViewport, useClass: MockCdkVirtualScrollViewport } ], imports: [ MatTableModule, CdkScrollingModule ] }).compileComponents(); });
2. 修正测试用例
原测试用例的问题在于调用ngAfterViewInit后才创建spy,且未模拟真实滚动场景。修正后测试用例如下:
it('should call fetchMore when scrolling to bottom', () => { // 获取Mock实例 const mockScroller = TestBed.inject(CdkVirtualScrollViewport) as MockCdkVirtualScrollViewport; // Spy待验证的fetchMore方法 spyOn(component, 'fetchMore'); // 设置组件必要属性,满足触发条件 component.maxItems = 20; component.listItems = new Array(10); // 初始化生命周期钩子 component.ngAfterViewInit(); // 第一次滚动:pairwise需要两次数据流才会输出 mockScroller.triggerScroll(); // 修改滚动偏移值,模拟滚动到底部 spyOn(mockScroller, 'measureScrollOffset').and.returnValue(100); // 第二次滚动,触发过滤逻辑 mockScroller.triggerScroll(); // 验证fetchMore被调用 expect(component.fetchMore).toHaveBeenCalled(); });
3. 关键注意点
- Mock类必须包含
itemSize属性,否则会触发初始化错误 - 使用
Subject模拟elementScrolled()的Observable流,方便手动控制滚动事件触发 pairwise()操作符需要至少两次数据流才会输出,测试时需触发两次滚动- 要根据测试场景调整
measureScrollOffset的返回值,确保满足组件中的过滤条件y2 < y1 && y2 < 140
内容的提问来源于stack exchange,提问作者Anand Raja
相关产品推荐
相关产品推荐

