angularunit-testingangular-materialkarma-jasmineangular-cdk-virtual-scroll

How to mock 'CdkVirtualScrollViewport' for infinite scrolling in Angular?


 @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();
      });
    })
  }
<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>

test case:

  it('should check \'ngAfterViewInit\'', () => {
    component.ngAfterViewInit();
    changeDetectorRef.detectChanges();
    spyOn(virtualScrollViewport, 'elementScrolled').and.callThrough();
    expect(virtualScrollViewport.elementScrolled).toHaveBeenCalled();
  });

The error trace what I got when mocking

        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)

Solution

  • A possible implementation of the mock class could be like this:

    import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
    import { Subject } from 'rxjs';
    
    export class MockCdkVirtualScrollViewport implements CdkVirtualScrollViewport {
      scrolledIndexChange: Subject<number> = new Subject<number>();
      elementScrolled: Subject<Event> = new Subject<Event>();
      setRenderedRange(): void { }
      getRenderedRange(): any { }
      // add any other properties and methods that are needed for your test
    }
    

    You can use it in your tests like so:

    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { Component } from '@angular/core';
    import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
    import { MockCdkVirtualScrollViewport } from './mock-cdk-virtual-scroll-viewport';
    
    describe('YourComponent', () => {
      let component: YourComponent;
      let fixture: ComponentFixture<YourComponent>;
      let virtualScrollViewport: CdkVirtualScrollViewport;
    
      beforeEach(async () => {
        // configure the test module with the mock class
        await TestBed.configureTestingModule({
          declarations: [ YourComponent ],
          providers: [
            { provide: CdkVirtualScrollViewport, useClass: MockCdkVirtualScrollViewport }
          ]
        }).compileComponents();
    
        // create an instance of the component and the mock class
        fixture = TestBed.createComponent(YourComponent);
        component = fixture.componentInstance;
        virtualScrollViewport = TestBed.inject(CdkVirtualScrollViewport);
      });
    
      it('should do something', () => {
        // use the methods and properties of the mock class in your test
        // for example you can call the `setRenderedRange()` method and check that it works as expected
      });
    });