8

I am using cdk-virtual-scroll-viewport + cdkVirtualFor in my component and it seems to work fine.

However in the unit test of that component the items don't get rendered.

I made a sample app based on this example and while the example works when you serve the app, the test I wrote fails.

app.module.ts

import { ScrollingModule } from '@angular/cdk/scrolling';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    ScrollingModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.ts

import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
  items = Array.from({length: 100000}).map((_, i) => `Item #${i}`);
}

app.component.html

<cdk-virtual-scroll-viewport itemSize="50" class="example-viewport">
  <div *cdkVirtualFor="let item of items" class="example-item">{{ item }}</div>
</cdk-virtual-scroll-viewport>

app.component.spec.ts

import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { ScrollingModule } from '@angular/cdk/scrolling';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
      imports: [
        ScrollingModule,
      ],
    }).compileComponents();
  }));

  it('should render at least 4 items', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement as HTMLElement;
    expect(compiled.querySelectorAll('.example-item').length).toBeGreaterThanOrEqual(4); // <-- Error: Expected 0 to be greater than or equal 4.
  });
});
moriesta
  • 1,170
  • 4
  • 19
  • 38

2 Answers2

13

Changing the unit test (see in the question) to following resolved it:

it('should render at least 4 items', fakeAsync(() => { // <---
  const fixture = TestBed.createComponent(AppComponent);
  fixture.autoDetectChanges(); // <--- 
  tick(500); // <---
  const compiled = fixture.debugElement.nativeElement as HTMLElement;
  expect(compiled.querySelectorAll('.example-item').length).toBeGreaterThanOrEqual(4);
}));
moriesta
  • 1,170
  • 4
  • 19
  • 38
  • Thanks. Saved a lot of time! – LppEdd May 11 '19 at 18:02
  • Seems to work! But can you please tell us why this magic happens? Or at least provide a good article from where we can learn? Why there is the need of those 3 statements? – Mateut Alin Jan 27 '21 at 12:26
  • The virtual scroll doesn't seem to render the items immediately (due to how it's designed inside; see the next answer and links there for more information), so we want to detect the changes automatically and wait a bit until that change happens. And you find plenty of resources on `fakeAsync` and related functions. Hope it helps! – moriesta Jan 27 '21 at 17:39
4

Somehow the solution from the previous answer didn't work for my case. Also the alternative solution proposed in github with flush() didn't work either.

So my final approach was to check how Google wrote their unit tests for the virtual viewport component. There I noticed that they used a function finishInit which is a local function in their unit test.

/** Finish initializing the virtual scroll component at the beginning of a test. */
function finishInit(fixture: ComponentFixture<any>) {
  // On the first cycle we render and measure the viewport.
  fixture.detectChanges();
  flush();

  // On the second cycle we render the items.
  fixture.detectChanges();
  flush();

  // Flush the initial fake scroll event.
  animationFrameScheduler.flush();
  flush();
  fixture.detectChanges();
}

Copied it into my unit test and that seems to work in my case. It also kinda explains what is happening in the backend.

cklam
  • 156
  • 5