From 87dde630c13488d191f778bbad049d4a2b55fc2f Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sun, 26 Apr 2026 09:30:03 +0200 Subject: [PATCH] refactor(cdk/scrolling): switch tests away from fakeAsync Reworks the CDK scrolling tests not to depend on `fakeAsync` anymore. --- src/cdk/scrolling/scroll-dispatcher.spec.ts | 8 +- src/cdk/scrolling/viewport-ruler.spec.ts | 10 +- .../scrolling/virtual-scroll-viewport.spec.ts | 669 +++++++++--------- .../virtual-scroll-viewport.zone.spec.ts | 33 +- 4 files changed, 357 insertions(+), 363 deletions(-) diff --git a/src/cdk/scrolling/scroll-dispatcher.spec.ts b/src/cdk/scrolling/scroll-dispatcher.spec.ts index a6ccb2765ce5..286ffb262af0 100644 --- a/src/cdk/scrolling/scroll-dispatcher.spec.ts +++ b/src/cdk/scrolling/scroll-dispatcher.spec.ts @@ -1,4 +1,4 @@ -import {TestBed, fakeAsync, ComponentFixture, tick} from '@angular/core/testing'; +import {TestBed, ComponentFixture} from '@angular/core/testing'; import {Component, ViewChild, ElementRef, ChangeDetectionStrategy} from '@angular/core'; import {CdkScrollable, ScrollDispatcher, ScrollingModule} from './public-api'; import {dispatchFakeEvent} from '../testing/private'; @@ -31,7 +31,7 @@ describe('ScrollDispatcher', () => { expect(scroll.scrollContainers.has(componentScrollable)).toBe(false); }); - it('should notify through the directive and service that a scroll event occurred', fakeAsync(() => { + it('should notify through the directive and service that a scroll event occurred', async () => { // Listen for notifications from scroll directive const scrollable = fixture.componentInstance.scrollable; const directiveSpy = jasmine.createSpy('directive scroll callback'); @@ -55,9 +55,9 @@ describe('ScrollDispatcher', () => { expect(serviceSpy).not.toHaveBeenCalled(); // After the throttle time, the notification should be sent. - tick(throttleTime); + await new Promise(resolve => setTimeout(resolve, throttleTime + 10)); expect(serviceSpy).toHaveBeenCalled(); - })); + }); it('should be able to unsubscribe from the global scrollable', () => { const spy = jasmine.createSpy('global scroll callback'); diff --git a/src/cdk/scrolling/viewport-ruler.spec.ts b/src/cdk/scrolling/viewport-ruler.spec.ts index bf068b6d23c3..110db7da7549 100644 --- a/src/cdk/scrolling/viewport-ruler.spec.ts +++ b/src/cdk/scrolling/viewport-ruler.spec.ts @@ -1,4 +1,4 @@ -import {TestBed, fakeAsync, tick} from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; import {dispatchFakeEvent} from '../testing/private'; import {ViewportRuler} from './viewport-ruler'; @@ -109,17 +109,17 @@ describe('ViewportRuler', () => { subscription.unsubscribe(); }); - it('should be able to throttle the callback', fakeAsync(() => { + it('should be able to throttle the callback', async () => { const spy = jasmine.createSpy('viewport changed spy'); - const subscription = viewportRuler.change(1337).subscribe(spy); + const subscription = viewportRuler.change(10).subscribe(spy); dispatchFakeEvent(window, 'resize'); expect(spy).not.toHaveBeenCalled(); - tick(1337); + await new Promise(resolve => setTimeout(resolve, 20)); expect(spy).toHaveBeenCalledTimes(1); subscription.unsubscribe(); - })); + }); }); }); diff --git a/src/cdk/scrolling/virtual-scroll-viewport.spec.ts b/src/cdk/scrolling/virtual-scroll-viewport.spec.ts index 7ef43e58f3a4..4a22f31b652d 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.spec.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.spec.ts @@ -16,18 +16,15 @@ import { inject, ChangeDetectionStrategy, } from '@angular/core'; -import { - ComponentFixture, - TestBed, - fakeAsync, - flush, - tick, - waitForAsync, -} from '@angular/core/testing'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {Subject} from 'rxjs'; import {dispatchFakeEvent} from '../testing/private'; describe('CdkVirtualScrollViewport', () => { + function wait(milliseconds: number) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); + } + describe('with FixedSizeVirtualScrollStrategy', () => { let fixture: ComponentFixture; let testComponent: FixedSizeVirtualScroll; @@ -39,8 +36,8 @@ describe('CdkVirtualScrollViewport', () => { viewport = testComponent.viewport; }); - it('should render initial state', fakeAsync(() => { - finishInit(fixture); + it('should render initial state', async () => { + await finishInit(fixture); const contentWrapper = viewport.elementRef.nativeElement.querySelector( '.cdk-virtual-scroll-content-wrapper', @@ -48,144 +45,144 @@ describe('CdkVirtualScrollViewport', () => { expect(contentWrapper.children.length) .withContext('should render 4 50px items to fill 200px space') .toBe(4); - })); + }); - it('should get the data length', fakeAsync(() => { - finishInit(fixture); + it('should get the data length', async () => { + await finishInit(fixture); expect(viewport.getDataLength()).toBe(testComponent.items.length); - })); + }); - it('should get the viewport size', fakeAsync(() => { - finishInit(fixture); + it('should get the viewport size', async () => { + await finishInit(fixture); expect(viewport.getViewportSize()).toBe(testComponent.viewportSize); - })); + }); - it('should update viewport size', fakeAsync(() => { + it('should update viewport size', async () => { testComponent.viewportSize = 300; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); viewport.checkViewportSize(); expect(viewport.getViewportSize()).toBe(300); testComponent.viewportSize = 500; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); viewport.checkViewportSize(); expect(viewport.getViewportSize()).toBe(500); - flush(); - })); + await fixture.whenStable(); + }); - it('should update the viewport size when the page viewport changes', fakeAsync(() => { - finishInit(fixture); + it('should update the viewport size when the page viewport changes', async () => { + await finishInit(fixture); spyOn(viewport, 'checkViewportSize').and.callThrough(); dispatchFakeEvent(window, 'resize'); fixture.detectChanges(); - tick(20); // The resize listener is debounced so we need to flush it. + await new Promise(resolve => setTimeout(resolve, 20)); // The resize listener is debounced so we need to flush it. expect(viewport.checkViewportSize).toHaveBeenCalled(); - })); + }); - it('should get the rendered range', fakeAsync(() => { - finishInit(fixture); + it('should get the rendered range', async () => { + await finishInit(fixture); expect(viewport.getRenderedRange()) .withContext('should render the first 4 50px items to fill 200px space') .toEqual({start: 0, end: 4}); - })); + }); - it('should contract the rendered range when changing to less data', fakeAsync(() => { - finishInit(fixture); + it('should contract the rendered range when changing to less data', async () => { + await finishInit(fixture); expect(viewport.getRenderedRange()).toEqual({start: 0, end: 4}); fixture.componentInstance.items = [0, 1]; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getRenderedRange()).toEqual({start: 0, end: 2}); fixture.componentInstance.items = []; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getRenderedRange()).toEqual({start: 0, end: 0}); - })); + }); - it('should get the rendered content offset', fakeAsync(() => { - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize + 5); + it('should get the rendered content offset', async () => { + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize + 5); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()) .withContext('should have 50px offset since first 50px item is not rendered') .toBe(testComponent.itemSize); - })); + }); - it('should get the scroll offset', fakeAsync(() => { - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize + 5); + it('should get the scroll offset', async () => { + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize + 5); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.measureScrollOffset()).toBe(testComponent.itemSize + 5); - })); + }); - it('should get the rendered content size', fakeAsync(() => { - finishInit(fixture); + it('should get the rendered content size', async () => { + await finishInit(fixture); expect(viewport.measureRenderedContentSize()) .withContext('should render 4 50px items with combined size of 200px to fill 200px space') .toBe(testComponent.viewportSize); - })); + }); - it('should measure range size', fakeAsync(() => { - finishInit(fixture); + it('should measure range size', async () => { + await finishInit(fixture); expect(viewport.measureRangeSize({start: 1, end: 3})) .withContext('combined size of 2 50px items should be 100px') .toBe(testComponent.itemSize * 2); - })); + }); - it('should measure range size when items has a margin', fakeAsync(() => { + it('should measure range size when items has a margin', async () => { fixture.componentInstance.hasMargin = true; - finishInit(fixture); + await finishInit(fixture); expect(viewport.measureRangeSize({start: 1, end: 3})) .withContext('combined size of 2 50px items with a 10px margin should be 110px') .toBe(testComponent.itemSize * 2 + 10); - })); + }); - it('should set total content size', fakeAsync(() => { - finishInit(fixture); + it('should set total content size', async () => { + await finishInit(fixture); viewport.setTotalContentSize(10000); - flush(); + await fixture.whenStable(); fixture.detectChanges(); expect(viewport.elementRef.nativeElement.scrollHeight).toBe(10000); - })); + }); - it('should set total content size in horizontal mode', fakeAsync(() => { + it('should set total content size in horizontal mode', async () => { testComponent.orientation = 'horizontal'; - finishInit(fixture); + await finishInit(fixture); viewport.setTotalContentSize(10000); - flush(); + await fixture.whenStable(); fixture.detectChanges(); expect(viewport.elementRef.nativeElement.scrollWidth).toBe(10000); - })); + }); - it('should set a class based on the orientation', fakeAsync(() => { - finishInit(fixture); + it('should set a class based on the orientation', async () => { + await finishInit(fixture); const viewportElement: HTMLElement = fixture.nativeElement.querySelector( '.cdk-virtual-scroll-viewport', ); @@ -197,22 +194,22 @@ describe('CdkVirtualScrollViewport', () => { fixture.detectChanges(); expect(viewportElement.classList).toContain('cdk-virtual-scroll-orientation-horizontal'); - })); + }); - it('should set the vertical class if an invalid orientation is set', fakeAsync(() => { + it('should set the vertical class if an invalid orientation is set', async () => { testComponent.orientation = 'diagonal' as any; - finishInit(fixture); + await finishInit(fixture); const viewportElement: HTMLElement = fixture.nativeElement.querySelector( '.cdk-virtual-scroll-viewport', ); expect(viewportElement.classList).toContain('cdk-virtual-scroll-orientation-vertical'); - })); + }); - it('should set rendered range', fakeAsync(() => { - finishInit(fixture); + it('should set rendered range', async () => { + await finishInit(fixture); viewport.setRenderedRange({start: 2, end: 3}); - flush(); + await fixture.whenStable(); fixture.detectChanges(); const items = fixture.elementRef.nativeElement.querySelectorAll('.item'); @@ -220,103 +217,105 @@ describe('CdkVirtualScrollViewport', () => { expect(items[0].innerText.trim()) .withContext('Expected item with index 2 to be rendered') .toBe('2 - 2'); - })); + }); - it('should set content offset to top of content', fakeAsync(() => { - finishInit(fixture); + it('should set content offset to top of content', async () => { + await finishInit(fixture); viewport.setRenderedContentOffset(10, 'to-start'); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()).toBe(10); - })); + }); - it('should set content offset to bottom of content', fakeAsync(() => { - finishInit(fixture); + it('should set content offset to bottom of content', async () => { + await finishInit(fixture); const contentSize = viewport.measureRenderedContentSize(); expect(contentSize).toBeGreaterThan(0); viewport.setRenderedContentOffset(contentSize + 10, 'to-end'); - flush(); + await fixture.whenStable(); + await new Promise(resolve => requestAnimationFrame(resolve)); // wait for afterNextRender expect(viewport.getOffsetToRenderedContentStart()).toBe(10); - })); + }); - it('should scroll to offset', fakeAsync(() => { - finishInit(fixture); + it('should scroll to offset', async () => { + await finishInit(fixture); viewport.scrollToOffset(testComponent.itemSize * 2); - triggerScroll(viewport); + await triggerScroll(viewport); fixture.detectChanges(); - flush(); + await fixture.whenStable(); + await wait(100); expect(viewport.measureScrollOffset()).toBe(testComponent.itemSize * 2); expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6}); - })); + }); - it('should scroll to index', fakeAsync(() => { - finishInit(fixture); + it('should scroll to index', async () => { + await finishInit(fixture); viewport.scrollToIndex(2); - triggerScroll(viewport); + await triggerScroll(viewport); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.measureScrollOffset()).toBe(testComponent.itemSize * 2); expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6}); - })); + }); - it('should scroll to offset in horizontal mode', fakeAsync(() => { + it('should scroll to offset in horizontal mode', async () => { testComponent.orientation = 'horizontal'; - finishInit(fixture); + await finishInit(fixture); viewport.scrollToOffset(testComponent.itemSize * 2); - triggerScroll(viewport); + await triggerScroll(viewport); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.measureScrollOffset()).toBe(testComponent.itemSize * 2); expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6}); - })); + }); - it('should scroll to index in horizontal mode', fakeAsync(() => { + it('should scroll to index in horizontal mode', async () => { testComponent.orientation = 'horizontal'; - finishInit(fixture); + await finishInit(fixture); viewport.scrollToIndex(2); - triggerScroll(viewport); + await triggerScroll(viewport); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.measureScrollOffset()).toBe(testComponent.itemSize * 2); expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6}); - })); + }); - it('should output scrolled index', fakeAsync(() => { - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize * 2 - 1); + it('should output scrolled index', async () => { + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize * 2 - 1); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(testComponent.scrolledToIndex).toBe(1); - triggerScroll(viewport, testComponent.itemSize * 2); + await triggerScroll(viewport, testComponent.itemSize * 2); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(testComponent.scrolledToIndex).toBe(2); - })); + }); - it('should update viewport as user scrolls down', fakeAsync(() => { - finishInit(fixture); + it('should update viewport as user scrolls down', async () => { + await finishInit(fixture); const maxOffset = testComponent.itemSize * testComponent.items.length - testComponent.viewportSize; for (let offset = 1; offset <= maxOffset; offset += 10) { - triggerScroll(viewport, offset); + await triggerScroll(viewport, offset); fixture.detectChanges(); - flush(); + await fixture.whenStable(); const expectedRange = { start: Math.floor(offset / testComponent.itemSize), @@ -334,17 +333,17 @@ describe('CdkVirtualScrollViewport', () => { .withContext(`rendered content size should match expected value at offset ${offset}`) .toBe((expectedRange.end - expectedRange.start) * testComponent.itemSize); } - })); + }); - it('should update viewport as user scrolls up', fakeAsync(() => { - finishInit(fixture); + it('should update viewport as user scrolls up', async () => { + await finishInit(fixture); const maxOffset = testComponent.itemSize * testComponent.items.length - testComponent.viewportSize; for (let offset = maxOffset - 1; offset >= 0; offset -= 10) { - triggerScroll(viewport, offset); + await triggerScroll(viewport, offset); fixture.detectChanges(); - flush(); + await fixture.whenStable(); const expectedRange = { start: Math.floor(offset / testComponent.itemSize), @@ -362,12 +361,12 @@ describe('CdkVirtualScrollViewport', () => { .withContext(`rendered content size should match expected value at offset ${offset}`) .toBe((expectedRange.end - expectedRange.start) * testComponent.itemSize); } - })); + }); - it('should render buffer element at the end when scrolled to the top', fakeAsync(() => { + it('should render buffer element at the end when scrolled to the top', async () => { testComponent.minBufferPx = testComponent.itemSize; testComponent.maxBufferPx = testComponent.itemSize; - finishInit(fixture); + await finishInit(fixture); expect(viewport.getRenderedRange()) .withContext( @@ -375,15 +374,16 @@ describe('CdkVirtualScrollViewport', () => { 'plus one buffer element at the end', ) .toEqual({start: 0, end: 5}); - })); + }); - it('should render buffer element at the start and end when scrolled to the middle', fakeAsync(() => { + it('should render buffer element at the start and end when scrolled to the middle', async () => { testComponent.minBufferPx = testComponent.itemSize; testComponent.maxBufferPx = testComponent.itemSize; - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize * 2); + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize * 2); fixture.detectChanges(); - flush(); + await fixture.whenStable(); + await wait(100); expect(viewport.getRenderedRange()) .withContext( @@ -391,15 +391,15 @@ describe('CdkVirtualScrollViewport', () => { 'buffer element at the start and end', ) .toEqual({start: 1, end: 7}); - })); + }); - it('should render buffer element at the start when scrolled to the bottom', fakeAsync(() => { + it('should render buffer element at the start when scrolled to the bottom', async () => { testComponent.minBufferPx = testComponent.itemSize; testComponent.maxBufferPx = testComponent.itemSize; - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize * 6); + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize * 6); fixture.detectChanges(); - flush(); + await wait(100); expect(viewport.getRenderedRange()) .withContext( @@ -407,13 +407,13 @@ describe('CdkVirtualScrollViewport', () => { 'buffer element at the start', ) .toEqual({start: 5, end: 10}); - })); + }); - it('should handle dynamic item size', fakeAsync(() => { - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize * 2); + it('should handle dynamic item size', async () => { + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize * 2); fixture.detectChanges(); - flush(); + await wait(100); expect(viewport.getRenderedRange()) .withContext('should render 4 50px items to fill 200px space') @@ -422,18 +422,18 @@ describe('CdkVirtualScrollViewport', () => { testComponent.itemSize *= 2; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await wait(100); expect(viewport.getRenderedRange()) .withContext('should render 2 100px items to fill 200px space') .toEqual({start: 1, end: 3}); - })); + }); - it('should handle dynamic buffer size', fakeAsync(() => { - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize * 2); + it('should handle dynamic buffer size', async () => { + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize * 2); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getRenderedRange()) .withContext('should render 4 50px items to fill 200px space') @@ -443,18 +443,18 @@ describe('CdkVirtualScrollViewport', () => { testComponent.maxBufferPx = testComponent.itemSize; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getRenderedRange()) .withContext('should expand to 1 buffer element on each side') .toEqual({start: 1, end: 7}); - })); + }); - it('should handle dynamic item array', fakeAsync(() => { - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize * 6); + it('should handle dynamic item array', async () => { + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize * 6); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()) .withContext('should be scrolled to bottom of 10 item list') @@ -463,18 +463,18 @@ describe('CdkVirtualScrollViewport', () => { testComponent.items = Array(5).fill(0); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()) .withContext('should be scrolled to bottom of 5 item list') .toBe(testComponent.itemSize); - })); + }); - it('should handle dynamic item array with dynamic buffer', fakeAsync(() => { - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize * 6); + it('should handle dynamic item array with dynamic buffer', async () => { + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize * 6); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()) .withContext('should be scrolled to bottom of 10 item list') @@ -486,19 +486,19 @@ describe('CdkVirtualScrollViewport', () => { fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()) .withContext('should render from first item') .toBe(0); - })); + }); - it('should handle dynamic item array keeping position when possible', fakeAsync(() => { + it('should handle dynamic item array keeping position when possible', async () => { testComponent.items = Array(100).fill(0); - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize * 50); + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize * 50); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()) .withContext('should be scrolled to index 50 item list') @@ -507,23 +507,23 @@ describe('CdkVirtualScrollViewport', () => { testComponent.items = Array(54).fill(0); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()) .withContext('should be kept the scroll position') .toBe(testComponent.itemSize * 50); - })); + }); - it('should update viewport as user scrolls right in horizontal mode', fakeAsync(() => { + it('should update viewport as user scrolls right in horizontal mode', async () => { testComponent.orientation = 'horizontal'; - finishInit(fixture); + await finishInit(fixture); const maxOffset = testComponent.itemSize * testComponent.items.length - testComponent.viewportSize; for (let offset = 1; offset <= maxOffset; offset += 10) { - triggerScroll(viewport, offset); + await triggerScroll(viewport, offset); fixture.detectChanges(); - flush(); + await fixture.whenStable(); const expectedRange = { start: Math.floor(offset / testComponent.itemSize), @@ -541,18 +541,18 @@ describe('CdkVirtualScrollViewport', () => { .withContext(`rendered content size should match expected value at offset ${offset}`) .toBe((expectedRange.end - expectedRange.start) * testComponent.itemSize); } - })); + }); - it('should update viewport as user scrolls left in horizontal mode', fakeAsync(() => { + it('should update viewport as user scrolls left in horizontal mode', async () => { testComponent.orientation = 'horizontal'; - finishInit(fixture); + await finishInit(fixture); const maxOffset = testComponent.itemSize * testComponent.items.length - testComponent.viewportSize; for (let offset = maxOffset - 1; offset >= 0; offset -= 10) { - triggerScroll(viewport, offset); + await triggerScroll(viewport, offset); fixture.detectChanges(); - flush(); + await fixture.whenStable(); const expectedRange = { start: Math.floor(offset / testComponent.itemSize), @@ -570,22 +570,22 @@ describe('CdkVirtualScrollViewport', () => { .withContext(`rendered content size should match expected value at offset ${offset}`) .toBe((expectedRange.end - expectedRange.start) * testComponent.itemSize); } - })); + }); - it('should work with a Set', fakeAsync(() => { + it('should work with a Set', async () => { const data = new Set(['hello', 'world', 'how', 'are', 'you']); testComponent.items = data as any; - finishInit(fixture); + await finishInit(fixture); expect(viewport.getRenderedRange()) .withContext('newly emitted items should be rendered') .toEqual({start: 0, end: 4}); - })); + }); - it('should work with an Observable', fakeAsync(() => { + it('should work with an Observable', async () => { const data = new Subject(); testComponent.items = data as any; - finishInit(fixture); + await finishInit(fixture); expect(viewport.getRenderedRange()) .withContext('no items should be rendered') @@ -593,17 +593,17 @@ describe('CdkVirtualScrollViewport', () => { data.next([1, 2, 3]); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getRenderedRange()) .withContext('newly emitted items should be rendered') .toEqual({start: 0, end: 3}); - })); + }); - it('should work with a DataSource', fakeAsync(() => { + it('should work with a DataSource', async () => { const data = new Subject(); testComponent.items = new ArrayDataSource(data) as any; - finishInit(fixture); + await finishInit(fixture); expect(viewport.getRenderedRange()) .withContext('no items should be rendered') @@ -611,14 +611,14 @@ describe('CdkVirtualScrollViewport', () => { data.next([1, 2, 3]); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getRenderedRange()) .withContext('newly emitted items should be rendered') .toEqual({start: 0, end: 3}); - })); + }); - it('should disconnect from data source on destroy', fakeAsync(() => { + it('should disconnect from data source on destroy', async () => { const data = new Subject(); const dataSource = new ArrayDataSource(data); @@ -626,69 +626,69 @@ describe('CdkVirtualScrollViewport', () => { spyOn(dataSource, 'disconnect').and.callThrough(); testComponent.items = dataSource as any; - finishInit(fixture); + await finishInit(fixture); expect(dataSource.connect).toHaveBeenCalled(); fixture.destroy(); expect(dataSource.disconnect).toHaveBeenCalled(); - })); + }); - it('should trackBy value by default', fakeAsync(() => { + it('should trackBy value by default', async () => { testComponent.items = []; spyOn(testComponent.virtualForOf._viewContainerRef, 'detach').and.callThrough(); - finishInit(fixture); + await finishInit(fixture); testComponent.items = [0]; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(testComponent.virtualForOf._viewContainerRef.detach).not.toHaveBeenCalled(); testComponent.items = [1]; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(testComponent.virtualForOf._viewContainerRef.detach).toHaveBeenCalled(); - })); + }); - it('should trackBy index when specified', fakeAsync(() => { + it('should trackBy index when specified', async () => { testComponent.trackBy = i => i; testComponent.items = []; spyOn(testComponent.virtualForOf._viewContainerRef, 'detach').and.callThrough(); - finishInit(fixture); + await finishInit(fixture); testComponent.items = [0]; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(testComponent.virtualForOf._viewContainerRef.detach).not.toHaveBeenCalled(); testComponent.items = [1]; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(testComponent.virtualForOf._viewContainerRef.detach).not.toHaveBeenCalled(); - })); + }); - it('should recycle views when template cache is large enough to accommodate', fakeAsync(() => { + it('should recycle views when template cache is large enough to accommodate', async () => { testComponent.trackBy = i => i; const spy = spyOn(testComponent.virtualForOf, '_getEmbeddedViewArgs').and.callThrough(); - finishInit(fixture); + await finishInit(fixture); // Should create views for the initial rendered items. expect(testComponent.virtualForOf._getEmbeddedViewArgs).toHaveBeenCalledTimes(4); spy.calls.reset(); - triggerScroll(viewport, 10); + await triggerScroll(viewport, 10); fixture.detectChanges(); - flush(); + await fixture.whenStable(); // As we first start to scroll we need to create one more item. This is because the first item // is still partially on screen and therefore can't be removed yet. At the same time a new @@ -699,30 +699,30 @@ describe('CdkVirtualScrollViewport', () => { const maxOffset = testComponent.itemSize * testComponent.items.length - testComponent.viewportSize; for (let offset = 10; offset <= maxOffset; offset += 10) { - triggerScroll(viewport, offset); + await triggerScroll(viewport, offset); fixture.detectChanges(); - flush(); + await fixture.whenStable(); } // As we scroll through the rest of the items, no new views should be created, our existing 5 // can just be recycled as appropriate. expect(testComponent.virtualForOf._getEmbeddedViewArgs).not.toHaveBeenCalled(); - })); + }); - it('should not recycle views when template cache is full', fakeAsync(() => { + it('should not recycle views when template cache is full', async () => { testComponent.trackBy = i => i; testComponent.templateCacheSize = 0; const spy = spyOn(testComponent.virtualForOf, '_getEmbeddedViewArgs').and.callThrough(); - finishInit(fixture); + await finishInit(fixture); // Should create views for the initial rendered items. expect(testComponent.virtualForOf._getEmbeddedViewArgs).toHaveBeenCalledTimes(4); spy.calls.reset(); - triggerScroll(viewport, 10); + await triggerScroll(viewport, 10); fixture.detectChanges(); - flush(); + await fixture.whenStable(); // As we first start to scroll we need to create one more item. This is because the first item // is still partially on screen and therefore can't be removed yet. At the same time a new @@ -733,93 +733,89 @@ describe('CdkVirtualScrollViewport', () => { const maxOffset = testComponent.itemSize * testComponent.items.length - testComponent.viewportSize; for (let offset = 10; offset <= maxOffset; offset += 10) { - triggerScroll(viewport, offset); + await triggerScroll(viewport, offset); fixture.detectChanges(); - flush(); + await fixture.whenStable(); } // Since our template cache size is 0, as we scroll through the rest of the items, we need to // create a new view for each one. expect(testComponent.virtualForOf._getEmbeddedViewArgs).toHaveBeenCalledTimes(5); - })); + }); - it('should render up to maxBufferPx when buffer dips below minBufferPx', fakeAsync(() => { + it('should render up to maxBufferPx when buffer dips below minBufferPx', async () => { testComponent.minBufferPx = testComponent.itemSize; testComponent.maxBufferPx = testComponent.itemSize * 2; - finishInit(fixture); + await finishInit(fixture); expect(viewport.getRenderedRange()) .withContext('should have 2 buffer items initially') .toEqual({start: 0, end: 6}); - triggerScroll(viewport, 50); + await triggerScroll(viewport, 50); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getRenderedRange()) .withContext('should not render additional buffer yet') .toEqual({start: 0, end: 6}); - triggerScroll(viewport, 51); + await triggerScroll(viewport, 51); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getRenderedRange()) .withContext('should render 2 more buffer items') .toEqual({start: 0, end: 8}); - })); + }); - it('should throw if maxBufferPx is less than minBufferPx', fakeAsync(() => { - expect(() => { - testComponent.minBufferPx = 100; - testComponent.maxBufferPx = 99; - finishInit(fixture); - }).toThrowError( + it('should throw if maxBufferPx is less than minBufferPx', async () => { + testComponent.minBufferPx = 100; + testComponent.maxBufferPx = 99; + await expectAsync(finishInit(fixture)).toBeRejectedWithError( 'CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx', ); - })); + }); - it('should register and degregister with ScrollDispatcher', fakeAsync(() => { + it('should register and degregister with ScrollDispatcher', async () => { const dispatcher = TestBed.inject(ScrollDispatcher); spyOn(dispatcher, 'register').and.callThrough(); spyOn(dispatcher, 'deregister').and.callThrough(); - finishInit(fixture); + await finishInit(fixture); expect(dispatcher.register).toHaveBeenCalledWith(testComponent.viewport.scrollable!); fixture.destroy(); expect(dispatcher.deregister).toHaveBeenCalledWith(testComponent.viewport.scrollable!); - })); + }); - it('should not throw when disposing of a view that will not fit in the cache', fakeAsync(() => { - finishInit(fixture); + it('should not throw when disposing of a view that will not fit in the cache', async () => { + await finishInit(fixture); testComponent.items = new Array(200).fill(0); testComponent.templateCacheSize = 1; // Reduce the cache size to something we can easily hit. fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); - - expect(() => { - for (let i = 0; i < 50; i++) { - viewport.scrollToIndex(i); - triggerScroll(viewport); - fixture.detectChanges(); - flush(); - } - }).not.toThrow(); - })); + await fixture.whenStable(); + + for (let i = 0; i < 50; i++) { + viewport.scrollToIndex(i); + await triggerScroll(viewport); + fixture.detectChanges(); + await fixture.whenStable(); + } + }); describe('viewChange change detection behavior', () => { - it('should not emit viewChange if there are no listeners', fakeAsync(() => { + it('should not emit viewChange if there are no listeners', async () => { const viewChangeSpy = spyOn(testComponent.virtualForOf.viewChange, 'next'); - finishInit(fixture); + await finishInit(fixture); testComponent.items = Array(10).fill(0); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); viewport.scrollToIndex(5); - triggerScroll(viewport); + await triggerScroll(viewport); expect(viewChangeSpy).not.toHaveBeenCalled(); - })); + }); }); }); @@ -840,97 +836,95 @@ describe('CdkVirtualScrollViewport', () => { ) as HTMLElement; }); - it( - 'should initially be scrolled all the way right and showing the first item in horizontal' + - ' mode', - fakeAsync(() => { - testComponent.orientation = 'horizontal'; - finishInit(fixture); - - expect(viewport.measureScrollOffset('right')).toBe(0); - expect(contentWrapperEl.style.transform).toMatch(/translateX\(0(px)?\)/); - expect((contentWrapperEl.children[0] as HTMLElement).innerText.trim()).toBe('0 - 0'); - }), - ); + it('should initially be scrolled all the way right and showing the first item in horizontal mode', async () => { + testComponent.orientation = 'horizontal'; + await finishInit(fixture); + + expect(viewport.measureScrollOffset('right')).toBe(0); + expect(contentWrapperEl.style.transform).toMatch(/translateX\(0(px)?\)/); + expect((contentWrapperEl.children[0] as HTMLElement).innerText.trim()).toBe('0 - 0'); + }); - it('should scroll through items as user scrolls to the left in horizontal mode', fakeAsync(() => { + it('should scroll through items as user scrolls to the left in horizontal mode', async () => { testComponent.orientation = 'horizontal'; - finishInit(fixture); + await finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize * testComponent.items.length); + await triggerScroll(viewport, testComponent.itemSize * testComponent.items.length); fixture.detectChanges(); - flush(); + await fixture.whenStable(); + await wait(100); expect(contentWrapperEl.style.transform).toBe('translateX(-300px)'); expect((contentWrapperEl.children[0] as HTMLElement).innerText.trim()).toBe('6 - 6'); - })); + }); - it('should interpret scrollToOffset amount as an offset from the right in horizontal mode', fakeAsync(() => { + it('should interpret scrollToOffset amount as an offset from the right in horizontal mode', async () => { testComponent.orientation = 'horizontal'; - finishInit(fixture); + await finishInit(fixture); viewport.scrollToOffset(100); - triggerScroll(viewport); + await triggerScroll(viewport); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.measureScrollOffset('right')).toBe(100); - })); + }); - it('should scroll to the correct index in horizontal mode', fakeAsync(() => { + it('should scroll to the correct index in horizontal mode', async () => { testComponent.orientation = 'horizontal'; - finishInit(fixture); + await finishInit(fixture); viewport.scrollToIndex(2); - triggerScroll(viewport); + await triggerScroll(viewport); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect((contentWrapperEl.children[0] as HTMLElement).innerText.trim()).toBe('2 - 2'); - })); + }); - it('should emit the scrolled to index in horizontal mode', fakeAsync(() => { + it('should emit the scrolled to index in horizontal mode', async () => { testComponent.orientation = 'horizontal'; - finishInit(fixture); + await finishInit(fixture); expect(testComponent.scrolledToIndex).toBe(0); viewport.scrollToIndex(2); - triggerScroll(viewport); + await triggerScroll(viewport); fixture.detectChanges(); - flush(); + await fixture.whenStable(); + await wait(100); expect(testComponent.scrolledToIndex).toBe(2); - })); + }); - it('should set total content size in RTL', fakeAsync(() => { - finishInit(fixture); + it('should set total content size in RTL', async () => { + await finishInit(fixture); viewport.setTotalContentSize(10000); - flush(); + await fixture.whenStable(); fixture.detectChanges(); expect(viewport.elementRef.nativeElement.scrollHeight).toBe(10000); - })); + }); - it('should set total content size in horizontal mode in RTL', fakeAsync(() => { + it('should set total content size in horizontal mode in RTL', async () => { testComponent.orientation = 'horizontal'; - finishInit(fixture); + await finishInit(fixture); viewport.setTotalContentSize(10000); - flush(); + await fixture.whenStable(); fixture.detectChanges(); expect(viewport.elementRef.nativeElement.scrollWidth).toBe(10000); - })); + }); }); describe('with no VirtualScrollStrategy', () => { - it('should fail on construction', fakeAsync(() => { + it('should fail on construction', async () => { expect(() => TestBed.createComponent(VirtualScrollWithNoStrategy)).toThrowError( 'Error: cdk-virtual-scroll-viewport requires the "itemSize" property to be set.', ); - })); + }); }); describe('with item that injects ViewContainerRef', () => { @@ -947,8 +941,8 @@ describe('CdkVirtualScrollViewport', () => { it( 'should render the values in the correct sequence when an item is ' + 'injecting ViewContainerRef', - fakeAsync(() => { - finishInit(fixture); + async () => { + await finishInit(fixture); const contentWrapper = viewport.elementRef.nativeElement.querySelector( '.cdk-virtual-scroll-content-wrapper', @@ -957,7 +951,7 @@ describe('CdkVirtualScrollViewport', () => { expect(Array.from(contentWrapper.children).map(child => child.textContent!.trim())).toEqual( ['0', '1', '2', '3', '4', '5', '6', '7'], ); - }), + }, ); }); @@ -972,19 +966,19 @@ describe('CdkVirtualScrollViewport', () => { viewport = testComponent.viewport; })); - it('should call custom trackBy when virtual for is added after init', fakeAsync(() => { - finishInit(fixture); + it('should call custom trackBy when virtual for is added after init', async () => { + await finishInit(fixture); expect(testComponent.trackBy).not.toHaveBeenCalled(); testComponent.renderVirtualFor = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - triggerScroll(viewport, testComponent.itemSize * 5); + await triggerScroll(viewport, testComponent.itemSize * 5); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(testComponent.trackBy).toHaveBeenCalled(); - })); + }); }); describe('with append only', () => { @@ -1002,64 +996,65 @@ describe('CdkVirtualScrollViewport', () => { ) as HTMLElement; })); - it('should not remove item that have already been rendered', fakeAsync(() => { - finishInit(fixture); + it('should not remove item that have already been rendered', async () => { + await finishInit(fixture); viewport.setRenderedRange({start: 100, end: 200}); fixture.detectChanges(); - flush(); + await fixture.whenStable(); viewport.setRenderedRange({start: 10, end: 50}); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getRenderedRange()).toEqual({start: 0, end: 200}); - })); + }); - it('rendered offset should always start at 0', fakeAsync(() => { - finishInit(fixture); - triggerScroll(viewport, testComponent.itemSize + 5); + it('rendered offset should always start at 0', async () => { + await finishInit(fixture); + await triggerScroll(viewport, testComponent.itemSize + 5); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()) .withContext('should have 0px offset as we are using appendOnly') .toBe(0); - })); + }); - it('should set content offset to bottom of content with append only', fakeAsync(() => { - finishInit(fixture); + it('should set content offset to bottom of content with append only', async () => { + await finishInit(fixture); const contentSize = viewport.measureRenderedContentSize(); expect(contentSize).toBeGreaterThan(0); viewport.setRenderedContentOffset(contentSize + 10, 'to-end'); - flush(); + await fixture.whenStable(); + await new Promise(resolve => requestAnimationFrame(resolve)); // wait for afterNextRender expect(viewport.getOffsetToRenderedContentStart()).toBe(0); - })); + }); - it('should set content offset to top of content with append only', fakeAsync(() => { - finishInit(fixture); + it('should set content offset to top of content with append only', async () => { + await finishInit(fixture); viewport.setRenderedContentOffset(10, 'to-start'); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()).toBe(0); - })); + }); - it('should not set a transform when scrolling', fakeAsync(() => { - finishInit(fixture); - triggerScroll(viewport, 0); + it('should not set a transform when scrolling', async () => { + await finishInit(fixture); + await triggerScroll(viewport, 0); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(contentWrapperEl.style.transform).toBe('translateY(0px)'); - triggerScroll(viewport, testComponent.itemSize * 10); + await triggerScroll(viewport, testComponent.itemSize * 10); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(contentWrapperEl.style.transform).toBe('translateY(0px)'); - })); + }); }); describe('with custom scrolling element', () => { @@ -1073,24 +1068,24 @@ describe('CdkVirtualScrollViewport', () => { viewport = testComponent.viewport; }); - it('should measure viewport offset', fakeAsync(() => { - finishInit(fixture); + it('should measure viewport offset', async () => { + await finishInit(fixture); expect(viewport.measureViewportOffset('top')) .withContext('with scrolling-element padding-top: 50 offset should be 50') .toBe(50); - })); + }); - it('should measure scroll offset with custom scrolling element', fakeAsync(() => { - finishInit(fixture); - triggerScroll(viewport, 100); + it('should measure scroll offset with custom scrolling element', async () => { + await finishInit(fixture); + await triggerScroll(viewport, 100); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.measureScrollOffset('top')) .withContext('should be 50 (actual scroll offset - viewport offset)') .toBe(50); - })); + }); }); describe('with scrollable window', () => { @@ -1104,18 +1099,18 @@ describe('CdkVirtualScrollViewport', () => { viewport = testComponent.viewport; }); - it('should measure scroll offset with scrollable window', fakeAsync(() => { - finishInit(fixture); + it('should measure scroll offset with scrollable window', async () => { + await finishInit(fixture); viewport.scrollToOffset(100 + 8); // the +8 is due to a horizontal scrollbar dispatchFakeEvent(window, 'scroll', true); - tick(); + await fixture.whenStable(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); expect(viewport.measureScrollOffset('top')) .withContext('should be 50 (actual scroll offset - viewport offset)') .toBe(50); - })); + }); }); it('should be able to query for a virtual scroll viewport as a CdkScrollable', () => { @@ -1127,28 +1122,28 @@ describe('CdkVirtualScrollViewport', () => { }); /** Finish initializing the virtual scroll component at the beginning of a test. */ -function finishInit(fixture: ComponentFixture) { +async function finishInit(fixture: ComponentFixture) { // On the first cycle we render and measure the viewport. fixture.detectChanges(); - flush(); + await fixture.whenStable(); // On the second cycle we render the items. fixture.detectChanges(); - flush(); + await fixture.whenStable(); // Flush the initial fake scroll event. - tick(16); // flush animation frame - flush(); + await new Promise(resolve => requestAnimationFrame(resolve)); + await fixture.whenStable(); fixture.detectChanges(); } /** Trigger a scroll event on the viewport (optionally setting a new scroll offset). */ -function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { +async function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { if (offset !== undefined) { viewport.scrollToOffset(offset); } dispatchFakeEvent(viewport.scrollable!.getElementRef().nativeElement, 'scroll'); - tick(16); // flush animation frame + await new Promise(resolve => setTimeout(resolve, 50)); } @Component({ diff --git a/src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts b/src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts index 4137a8e74fa6..71940e599fa5 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts @@ -9,8 +9,7 @@ import { provideZoneChangeDetection, ChangeDetectionStrategy, } from '@angular/core'; -import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; -import {animationFrameScheduler} from 'rxjs'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; import {dispatchFakeEvent} from '../testing/private'; import {ScrollingModule} from './scrolling-module'; import {CdkVirtualForOf} from './virtual-for-of'; @@ -32,30 +31,30 @@ describe('CdkVirtualScrollViewport Zone.js intergation', () => { viewport = testComponent.viewport; }); - it('should emit on viewChange inside the Angular zone', fakeAsync(() => { + it('should emit on viewChange inside the Angular zone', async () => { const zoneTest = jasmine.createSpy('zone test'); testComponent.virtualForOf.viewChange.subscribe(() => zoneTest(NgZone.isInAngularZone())); - finishInit(fixture); + await finishInit(fixture); expect(zoneTest).toHaveBeenCalledWith(true); - })); + }); describe('viewChange change detection behavior', () => { - it('should run change detection if there are any viewChange listeners', fakeAsync(() => { + it('should run change detection if there are any viewChange listeners', async () => { testComponent.virtualForOf.viewChange.subscribe(); - finishInit(fixture); + await finishInit(fixture); testComponent.items = Array(10).fill(0); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); const spy = jasmine.createSpy(); afterNextRender(spy, {injector: TestBed.inject(Injector)}); viewport.scrollToIndex(5); - triggerScroll(viewport); + await triggerScroll(viewport); expect(spy).toHaveBeenCalledTimes(1); - })); + }); }); }); }); @@ -132,29 +131,29 @@ class FixedSizeVirtualScroll { } /** Finish initializing the virtual scroll component at the beginning of a test. */ -function finishInit(fixture: ComponentFixture) { +async function finishInit(fixture: ComponentFixture) { // On the first cycle we render and measure the viewport. fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); // On the second cycle we render the items. fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); - flush(); + await fixture.whenStable(); // Flush the initial fake scroll event. - animationFrameScheduler.flush(); - flush(); + await new Promise(resolve => requestAnimationFrame(resolve)); + await fixture.whenStable(); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); } /** Trigger a scroll event on the viewport (optionally setting a new scroll offset). */ -function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { +async function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { if (offset !== undefined) { viewport.scrollToOffset(offset); } dispatchFakeEvent(viewport.scrollable.getElementRef().nativeElement, 'scroll'); - animationFrameScheduler.flush(); + await new Promise(resolve => setTimeout(resolve, 16)); }