You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 11:50:24