diff --git a/src/cdk/overlay/overlay.spec.ts b/src/cdk/overlay/overlay.spec.ts index dee7c159babe..47a700d22740 100644 --- a/src/cdk/overlay/overlay.spec.ts +++ b/src/cdk/overlay/overlay.spec.ts @@ -1,37 +1,35 @@ -import { - waitForAsync, - fakeAsync, - tick, - ComponentFixture, - TestBed, - flush, -} from '@angular/core/testing'; +import {Direction, Directionality} from '@angular/cdk/bidi'; +import {CdkPortal, ComponentPortal, TemplatePortal} from '@angular/cdk/portal'; +import {Location} from '@angular/common'; +import {SpyLocation} from '@angular/common/testing'; import { Component, - ViewChild, - ViewContainerRef, ErrorHandler, - Injectable, EventEmitter, - NgZone, + Injectable, Type, - provideZoneChangeDetection, + ViewChild, + ViewContainerRef, } from '@angular/core'; -import {Direction, Directionality} from '@angular/cdk/bidi'; +import { + ComponentFixture, + TestBed, + fakeAsync, + flush, + tick, + waitForAsync, +} from '@angular/core/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {dispatchFakeEvent} from '../testing/private'; -import {ComponentPortal, TemplatePortal, CdkPortal} from '@angular/cdk/portal'; -import {Location} from '@angular/common'; -import {SpyLocation} from '@angular/common/testing'; import { Overlay, + OverlayConfig, OverlayContainer, OverlayModule, OverlayRef, - OverlayConfig, PositionStrategy, ScrollStrategy, } from './index'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; describe('Overlay', () => { let overlay: Overlay; @@ -48,8 +46,6 @@ describe('Overlay', () => { TestBed.configureTestingModule({ imports: [OverlayModule, ...imports], providers: [ - provideZoneChangeDetection(), - provideZoneChangeDetection(), { provide: Directionality, useFactory: () => { @@ -949,12 +945,12 @@ describe('Overlay', () => { .toContain('custom-panel-class'); }); - it('should wait for the overlay to be detached before removing the panelClass', () => { + it('should wait for the overlay to be detached before removing the panelClass', async () => { const config = new OverlayConfig({panelClass: 'custom-panel-class'}); const overlayRef = overlay.create(config); overlayRef.attach(componentPortal); - viewContainerFixture.detectChanges(); + await viewContainerFixture.whenStable(); const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; expect(pane.classList) @@ -962,13 +958,10 @@ describe('Overlay', () => { .toContain('custom-panel-class'); overlayRef.detach(); - // Stable emits after zone.run - TestBed.inject(NgZone).run(() => { - viewContainerFixture.detectChanges(); - expect(pane.classList) - .withContext('Expected class not to be removed immediately') - .toContain('custom-panel-class'); - }); + expect(pane.classList) + .withContext('Expected class not to be removed immediately') + .toContain('custom-panel-class'); + await viewContainerFixture.whenStable(); expect(pane.classList) .not.withContext('Expected class to be removed on stable') diff --git a/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts b/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts index eab3daf2c83e..54c404ff8f11 100644 --- a/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts +++ b/src/cdk/overlay/scroll/close-scroll-strategy.spec.ts @@ -1,9 +1,9 @@ -import {inject, TestBed, fakeAsync} from '@angular/core/testing'; -import {Component, ElementRef, NgZone, provideZoneChangeDetection} from '@angular/core'; -import {Subject} from 'rxjs'; import {ComponentPortal, PortalModule} from '@angular/cdk/portal'; import {CdkScrollable, ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling'; -import {Overlay, OverlayConfig, OverlayRef, OverlayModule, OverlayContainer} from '../index'; +import {Component, ElementRef} from '@angular/core'; +import {TestBed, fakeAsync, inject} from '@angular/core/testing'; +import {Subject} from 'rxjs'; +import {Overlay, OverlayConfig, OverlayContainer, OverlayModule, OverlayRef} from '../index'; describe('CloseScrollStrategy', () => { let overlayRef: OverlayRef; @@ -17,7 +17,6 @@ describe('CloseScrollStrategy', () => { TestBed.configureTestingModule({ imports: [OverlayModule, PortalModule, MozarellaMsg], providers: [ - provideZoneChangeDetection(), { provide: ScrollDispatcher, useFactory: () => ({ @@ -75,17 +74,6 @@ describe('CloseScrollStrategy', () => { expect(overlayRef.detach).not.toHaveBeenCalled(); }); - it('should detach inside the NgZone', () => { - const spy = jasmine.createSpy('detachment spy'); - const subscription = overlayRef.detachments().subscribe(() => spy(NgZone.isInAngularZone())); - - overlayRef.attach(componentPortal); - scrolledSubject.next(); - - expect(spy).toHaveBeenCalledWith(true); - subscription.unsubscribe(); - }); - it('should be able to reposition the overlay up to a certain threshold before closing', inject( [Overlay], (overlay: Overlay) => { diff --git a/src/cdk/overlay/scroll/close-scroll-strategy.zone.spec.ts b/src/cdk/overlay/scroll/close-scroll-strategy.zone.spec.ts new file mode 100644 index 000000000000..85c85a5dd92c --- /dev/null +++ b/src/cdk/overlay/scroll/close-scroll-strategy.zone.spec.ts @@ -0,0 +1,72 @@ +import {ComponentPortal, PortalModule} from '@angular/cdk/portal'; +import {Component, NgZone, provideZoneChangeDetection} from '@angular/core'; +import {TestBed, fakeAsync, inject} from '@angular/core/testing'; +import {Subject} from 'rxjs'; +import {Overlay} from '../overlay'; +import {OverlayConfig} from '../overlay-config'; +import {OverlayContainer} from '../overlay-container'; +import {OverlayModule} from '../overlay-module'; +import {OverlayRef} from '../overlay-ref'; +import {CdkScrollable, ScrollDispatcher, ViewportRuler} from '../public-api'; + +describe('CloseScrollStrategy Zone.js integration', () => { + let overlayRef: OverlayRef; + let componentPortal: ComponentPortal; + let scrolledSubject = new Subject(); + let scrollPosition: number; + + beforeEach(fakeAsync(() => { + scrollPosition = 0; + + TestBed.configureTestingModule({ + imports: [OverlayModule, PortalModule, MozarellaMsg], + providers: [ + provideZoneChangeDetection(), + { + provide: ScrollDispatcher, + useFactory: () => ({ + scrolled: () => scrolledSubject, + }), + }, + { + provide: ViewportRuler, + useFactory: () => ({ + getViewportScrollPosition: () => ({top: scrollPosition}), + }), + }, + ], + }); + + TestBed.compileComponents(); + })); + + beforeEach(inject([Overlay], (overlay: Overlay) => { + let overlayConfig = new OverlayConfig({scrollStrategy: overlay.scrollStrategies.close()}); + overlayRef = overlay.create(overlayConfig); + componentPortal = new ComponentPortal(MozarellaMsg); + })); + + afterEach(inject([OverlayContainer], (container: OverlayContainer) => { + overlayRef.dispose(); + container.getContainerElement().remove(); + })); + + it('should detach inside the NgZone', () => { + const spy = jasmine.createSpy('detachment spy'); + const subscription = overlayRef.detachments().subscribe(() => spy(NgZone.isInAngularZone())); + + overlayRef.attach(componentPortal); + scrolledSubject.next(); + + expect(spy).toHaveBeenCalledWith(true); + subscription.unsubscribe(); + }); +}); + +/** Simple component that we can attach to the overlay. */ +@Component({ + template: '

Mozarella

', + standalone: true, + imports: [OverlayModule, PortalModule], +}) +class MozarellaMsg {} diff --git a/src/cdk/portal/portal.spec.ts b/src/cdk/portal/portal.spec.ts index 12f3f7a86079..e7d4fe54d1b0 100644 --- a/src/cdk/portal/portal.spec.ts +++ b/src/cdk/portal/portal.spec.ts @@ -1,9 +1,11 @@ import {CommonModule} from '@angular/common'; import { + AfterViewInit, ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, + Directive, ElementRef, Injector, Optional, @@ -13,11 +15,8 @@ import { ViewChild, ViewChildren, ViewContainerRef, - Directive, - AfterViewInit, - provideZoneChangeDetection, } from '@angular/core'; -import {ComponentFixture, inject, TestBed} from '@angular/core/testing'; +import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; import {DomPortalOutlet} from './dom-portal-outlet'; import {ComponentPortal, DomPortal, Portal, TemplatePortal} from './portal'; import {CdkPortal, CdkPortalOutlet, PortalModule} from './portal-directives'; @@ -25,7 +24,6 @@ import {CdkPortal, CdkPortalOutlet, PortalModule} from './portal-directives'; describe('Portals', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ PortalModule, CommonModule, @@ -58,6 +56,7 @@ describe('Portals', () => { let hostContainer = fixture.nativeElement.querySelector('.portal-container'); testAppComponent.selectedPortal = componentPortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -75,6 +74,7 @@ describe('Portals', () => { let templatePortal = new TemplatePortal(testAppComponent.templateRef, null!); testAppComponent.selectedPortal = templatePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present and no context is projected @@ -103,6 +103,7 @@ describe('Portals', () => { ); testAppComponent.selectedPortal = templatePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present and no context is projected @@ -136,6 +137,7 @@ describe('Portals', () => { .toBe(false); testAppComponent.selectedPortal = domPortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(domPortal.element.parentNode).not.toBe( @@ -148,6 +150,7 @@ describe('Portals', () => { expect(testAppComponent.portalOutlet.hasAttached()).toBe(true); testAppComponent.selectedPortal = undefined; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(domPortal.element.parentNode) @@ -166,6 +169,7 @@ describe('Portals', () => { expect(() => { testAppComponent.selectedPortal = domPortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }).toThrowError('DOM portal content must be attached to a parent node.'); }); @@ -176,12 +180,14 @@ describe('Portals', () => { const domPortal = new DomPortal(testAppComponent.domPortalContent); testAppComponent.selectedPortal = domPortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); parent.innerHTML = ''; expect(() => { testAppComponent.selectedPortal = undefined; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); }).not.toThrow(); }); @@ -193,12 +199,14 @@ describe('Portals', () => { // TemplatePortal without context: let templatePortal = new TemplatePortal(testAppComponent.templateRef, null!); testAppComponent.selectedPortal = templatePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present and NO context is projected expect(hostContainer.textContent).toContain('Banana - !'); // using TemplatePortal.attach method to set context testAppComponent.selectedPortal = undefined; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); templatePortal.attach(testAppComponent.portalOutlet, {$implicit: {status: 'rotten'}}); fixture.detectChanges(); @@ -211,6 +219,7 @@ describe('Portals', () => { $implicit: {status: 'fresh'}, }); testAppComponent.selectedPortal = templatePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present and context given via the // constructor is projected @@ -219,6 +228,7 @@ describe('Portals', () => { // using TemplatePortal constructor to set the context but also calling attach method with // context, the latter should take precedence: testAppComponent.selectedPortal = undefined; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); templatePortal.attach(testAppComponent.portalOutlet, {$implicit: {status: 'rotten'}}); fixture.detectChanges(); @@ -231,6 +241,7 @@ describe('Portals', () => { // Set the selectedHost to be a ComponentPortal. let testAppComponent = fixture.componentInstance; testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testAppComponent.selectedPortal.isAttached).toBe(true); @@ -246,6 +257,7 @@ describe('Portals', () => { // Set the selectedHost to be a ComponentPortal. let testAppComponent = fixture.componentInstance; testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg, undefined, chocolateInjector); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -262,6 +274,7 @@ describe('Portals', () => { // Set the selectedHost to be a TemplatePortal. testAppComponent.selectedPortal = testAppComponent.cakePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -277,6 +290,7 @@ describe('Portals', () => { // Set the selectedHost to be a TemplatePortal (with the `*` syntax). testAppComponent.selectedPortal = testAppComponent.piePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -292,6 +306,7 @@ describe('Portals', () => { // Set the selectedHost to be a TemplatePortal. testAppComponent.selectedPortal = testAppComponent.portalWithBinding; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -302,6 +317,7 @@ describe('Portals', () => { // When updating the binding value. testAppComponent.fruit = 'Mango'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect the new value to be reflected in the rendered output. @@ -316,6 +332,7 @@ describe('Portals', () => { // Set the selectedHost to be a TemplatePortal. testAppComponent.selectedPortal = testAppComponent.portalWithTemplate; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -324,6 +341,7 @@ describe('Portals', () => { // When updating the binding value. testAppComponent.fruits = ['Mangosteen']; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect the new value to be reflected in the rendered output. @@ -338,6 +356,7 @@ describe('Portals', () => { // Set the selectedHost to be a ComponentPortal. testAppComponent.selectedPortal = testAppComponent.piePortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect that the content of the attached portal is present. @@ -345,6 +364,7 @@ describe('Portals', () => { expect(hostContainer.textContent).toContain('Pie'); testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(hostContainer.textContent).toContain('Pizza'); @@ -353,12 +373,14 @@ describe('Portals', () => { it('should detach the portal when it is set to null', () => { let testAppComponent = fixture.componentInstance; testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testAppComponent.portalOutlet.hasAttached()).toBe(true); expect(testAppComponent.portalOutlet.portal).toBe(testAppComponent.selectedPortal); testAppComponent.selectedPortal = null!; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testAppComponent.portalOutlet.hasAttached()).toBe(false); @@ -387,6 +409,7 @@ describe('Portals', () => { let testAppComponent = fixture.componentInstance; testAppComponent.selectedPortal = new ComponentPortal(PizzaMsg); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(testAppComponent.portalOutlet.portal).toBeTruthy(); @@ -449,6 +472,7 @@ describe('Portals', () => { const portal = new ComponentPortal(PizzaMsg, fixture.componentInstance.alternateContainer); fixture.componentInstance.selectedPortal = portal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(hostContainer.textContent).toContain('Pizza'); @@ -462,6 +486,7 @@ describe('Portals', () => { ]); testAppComponent.selectedPortal = componentPortal; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('Projectable node'); @@ -586,6 +611,7 @@ describe('Portals', () => { // When updating the binding value. testAppComponent.fruit = 'Mango'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Expect the new value to be reflected in the rendered output. diff --git a/src/cdk/scrolling/scrollable.spec.ts b/src/cdk/scrolling/scrollable.spec.ts index 495e3804e5a1..2a09d7863ef7 100644 --- a/src/cdk/scrolling/scrollable.spec.ts +++ b/src/cdk/scrolling/scrollable.spec.ts @@ -1,14 +1,7 @@ import {Direction} from '@angular/cdk/bidi'; import {CdkScrollable, ScrollingModule} from '@angular/cdk/scrolling'; -import { - Component, - ElementRef, - Input, - ViewChild, - NgZone, - provideZoneChangeDetection, -} from '@angular/core'; -import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, ElementRef, Input, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; function expectOverlapping(el1: ElementRef, el2: ElementRef, expected = true) { const r1 = el1.nativeElement.getBoundingClientRect(); @@ -33,10 +26,6 @@ describe('CdkScrollable', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [ - provideZoneChangeDetection(), - {provide: NgZone, useFactory: () => new NgZone({})}, - ], imports: [ScrollingModule, ScrollableViewport], }).compileComponents(); })); @@ -140,6 +129,7 @@ describe('CdkScrollable', () => { describe('in RTL context', () => { beforeEach(() => { testComponent.dir = 'rtl'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); maxOffset = testComponent.scrollContainer.nativeElement.scrollHeight - diff --git a/src/cdk/scrolling/virtual-scroll-viewport.spec.ts b/src/cdk/scrolling/virtual-scroll-viewport.spec.ts index 27c373979b2b..fca855affe4b 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.spec.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.spec.ts @@ -7,28 +7,26 @@ import { ScrollingModule, } from '@angular/cdk/scrolling'; import {CommonModule} from '@angular/common'; -import {dispatchFakeEvent} from '../testing/private'; import { + ApplicationRef, Component, - NgZone, + Directive, TrackByFunction, ViewChild, - ViewEncapsulation, - Directive, ViewContainerRef, - ApplicationRef, - provideZoneChangeDetection, + ViewEncapsulation, } from '@angular/core'; import { - waitForAsync, ComponentFixture, + TestBed, fakeAsync, flush, inject, - TestBed, tick, + waitForAsync, } from '@angular/core/testing'; -import {animationFrameScheduler, Subject} from 'rxjs'; +import {Subject} from 'rxjs'; +import {dispatchFakeEvent} from '../testing/private'; describe('CdkVirtualScrollViewport', () => { describe('with FixedSizeVirtualScrollStrategy', () => { @@ -38,7 +36,6 @@ describe('CdkVirtualScrollViewport', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ScrollingModule, FixedSizeVirtualScroll], }).compileComponents(); })); @@ -74,12 +71,14 @@ describe('CdkVirtualScrollViewport', () => { it('should update viewport size', fakeAsync(() => { testComponent.viewportSize = 300; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); viewport.checkViewportSize(); expect(viewport.getViewportSize()).toBe(300); testComponent.viewportSize = 500; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); viewport.checkViewportSize(); @@ -111,11 +110,13 @@ describe('CdkVirtualScrollViewport', () => { expect(viewport.getRenderedRange()).toEqual({start: 0, end: 4}); fixture.componentInstance.items = [0, 1]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(viewport.getRenderedRange()).toEqual({start: 0, end: 2}); fixture.componentInstance.items = []; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(viewport.getRenderedRange()).toEqual({start: 0, end: 0}); @@ -196,6 +197,7 @@ describe('CdkVirtualScrollViewport', () => { expect(viewportElement.classList).toContain('cdk-virtual-scroll-orientation-vertical'); testComponent.orientation = 'horizontal'; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(viewportElement.classList).toContain('cdk-virtual-scroll-orientation-horizontal'); @@ -214,8 +216,8 @@ describe('CdkVirtualScrollViewport', () => { it('should set rendered range', fakeAsync(() => { finishInit(fixture); viewport.setRenderedRange({start: 2, end: 3}); - fixture.detectChanges(); flush(); + fixture.detectChanges(); const items = fixture.elementRef.nativeElement.querySelectorAll('.item'); expect(items.length).withContext('Expected 1 item to be rendered').toBe(1); @@ -233,15 +235,15 @@ describe('CdkVirtualScrollViewport', () => { expect(viewport.getOffsetToRenderedContentStart()).toBe(10); })); - it('should set content offset to bottom of content', fakeAsync(() => { + it('should set content offset to bottom of content', fakeAsync(async () => { finishInit(fixture); const contentSize = viewport.measureRenderedContentSize(); expect(contentSize).toBeGreaterThan(0); viewport.setRenderedContentOffset(contentSize + 10, 'to-end'); - fixture.detectChanges(); flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()).toBe(10); })); @@ -423,6 +425,7 @@ describe('CdkVirtualScrollViewport', () => { .toEqual({start: 2, end: 6}); testComponent.itemSize *= 2; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -443,6 +446,7 @@ describe('CdkVirtualScrollViewport', () => { testComponent.minBufferPx = testComponent.itemSize; testComponent.maxBufferPx = testComponent.itemSize; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -462,6 +466,7 @@ describe('CdkVirtualScrollViewport', () => { .toBe(testComponent.itemSize * 6); testComponent.items = Array(5).fill(0); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -484,6 +489,7 @@ describe('CdkVirtualScrollViewport', () => { testComponent.minBufferPx = testComponent.itemSize; testComponent.maxBufferPx = testComponent.itemSize; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -504,6 +510,7 @@ describe('CdkVirtualScrollViewport', () => { .toBe(testComponent.itemSize * 50); testComponent.items = Array(54).fill(0); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -639,12 +646,14 @@ describe('CdkVirtualScrollViewport', () => { finishInit(fixture); testComponent.items = [0]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); expect(testComponent.virtualForOf._viewContainerRef.detach).not.toHaveBeenCalled(); testComponent.items = [1]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -658,12 +667,14 @@ describe('CdkVirtualScrollViewport', () => { finishInit(fixture); testComponent.items = [0]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); expect(testComponent.virtualForOf._viewContainerRef.detach).not.toHaveBeenCalled(); testComponent.items = [1]; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -764,9 +775,13 @@ describe('CdkVirtualScrollViewport', () => { })); it('should throw if maxBufferPx is less than minBufferPx', fakeAsync(() => { - testComponent.minBufferPx = 100; - testComponent.maxBufferPx = 99; - expect(() => finishInit(fixture)).toThrow(); + expect(() => { + testComponent.minBufferPx = 100; + testComponent.maxBufferPx = 99; + finishInit(fixture); + }).toThrowError( + 'CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx', + ); })); it('should register and degregister with ScrollDispatcher', fakeAsync( @@ -780,17 +795,11 @@ describe('CdkVirtualScrollViewport', () => { }), )); - it('should emit on viewChange inside the Angular zone', fakeAsync(() => { - const zoneTest = jasmine.createSpy('zone test'); - testComponent.virtualForOf.viewChange.subscribe(() => zoneTest(NgZone.isInAngularZone())); - finishInit(fixture); - expect(zoneTest).toHaveBeenCalledWith(true); - })); - it('should not throw when disposing of a view that will not fit in the cache', fakeAsync(() => { 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(); @@ -814,6 +823,7 @@ describe('CdkVirtualScrollViewport', () => { it('should not run change detection if there are no viewChange listeners', fakeAsync(() => { finishInit(fixture); testComponent.items = Array(10).fill(0); + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); @@ -824,21 +834,6 @@ describe('CdkVirtualScrollViewport', () => { expect(appRef.tick).not.toHaveBeenCalled(); })); - - it('should run change detection if there are any viewChange listeners', fakeAsync(() => { - testComponent.virtualForOf.viewChange.subscribe(); - finishInit(fixture); - testComponent.items = Array(10).fill(0); - fixture.detectChanges(); - flush(); - - spyOn(appRef, 'tick'); - - viewport.scrollToIndex(5); - triggerScroll(viewport); - - expect(appRef.tick).toHaveBeenCalledTimes(1); - })); }); }); @@ -851,7 +846,6 @@ describe('CdkVirtualScrollViewport', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ScrollingModule, FixedSizeVirtualScrollWithRtlDirection], }).compileComponents(); @@ -952,7 +946,6 @@ describe('CdkVirtualScrollViewport', () => { describe('with no VirtualScrollStrategy', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ScrollingModule, VirtualScrollWithNoStrategy], }).compileComponents(); }); @@ -971,7 +964,6 @@ describe('CdkVirtualScrollViewport', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ ScrollingModule, VirtualScrollWithItemInjectingViewContainer, @@ -1010,7 +1002,6 @@ describe('CdkVirtualScrollViewport', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ScrollingModule, CommonModule, DelayedInitializationVirtualScroll], }).compileComponents(); fixture = TestBed.createComponent(DelayedInitializationVirtualScroll); @@ -1023,6 +1014,7 @@ describe('CdkVirtualScrollViewport', () => { expect(testComponent.trackBy).not.toHaveBeenCalled(); testComponent.renderVirtualFor = true; + fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); triggerScroll(viewport, testComponent.itemSize * 5); fixture.detectChanges(); @@ -1040,7 +1032,6 @@ describe('CdkVirtualScrollViewport', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - providers: [provideZoneChangeDetection()], imports: [ScrollingModule, CommonModule, VirtualScrollWithAppendOnly], }).compileComponents(); fixture = TestBed.createComponent(VirtualScrollWithAppendOnly); @@ -1074,15 +1065,15 @@ describe('CdkVirtualScrollViewport', () => { .toBe(0); })); - it('should set content offset to bottom of content', fakeAsync(() => { + it('should set content offset to bottom of content', fakeAsync(async () => { finishInit(fixture); const contentSize = viewport.measureRenderedContentSize(); expect(contentSize).toBeGreaterThan(0); viewport.setRenderedContentOffset(contentSize + 10, 'to-end'); - fixture.detectChanges(); flush(); + await fixture.whenStable(); expect(viewport.getOffsetToRenderedContentStart()).toBe(0); })); @@ -1203,7 +1194,7 @@ function finishInit(fixture: ComponentFixture) { flush(); // Flush the initial fake scroll event. - animationFrameScheduler.flush(); + tick(16); // flush animation frame flush(); fixture.detectChanges(); } @@ -1214,7 +1205,7 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { viewport.scrollToOffset(offset); } dispatchFakeEvent(viewport.scrollable.getElementRef().nativeElement, 'scroll'); - animationFrameScheduler.flush(); + tick(16); // flush animation frame } @Component({ diff --git a/src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts b/src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts new file mode 100644 index 000000000000..a1d9dfbb055d --- /dev/null +++ b/src/cdk/scrolling/virtual-scroll-viewport.zone.spec.ts @@ -0,0 +1,173 @@ +import { + ApplicationRef, + Component, + NgZone, + TrackByFunction, + ViewChild, + ViewEncapsulation, + provideZoneChangeDetection, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + fakeAsync, + flush, + inject, + waitForAsync, +} from '@angular/core/testing'; +import {animationFrameScheduler} from 'rxjs'; +import {dispatchFakeEvent} from '../testing/private'; +import {ScrollingModule} from './scrolling-module'; +import {CdkVirtualForOf} from './virtual-for-of'; +import {CdkVirtualScrollViewport} from './virtual-scroll-viewport'; + +describe('CdkVirtualScrollViewport Zone.js intergation', () => { + describe('with FixedSizeVirtualScrollStrategy', () => { + let fixture: ComponentFixture; + let testComponent: FixedSizeVirtualScroll; + let viewport: CdkVirtualScrollViewport; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + providers: [provideZoneChangeDetection()], + imports: [ScrollingModule, FixedSizeVirtualScroll], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FixedSizeVirtualScroll); + testComponent = fixture.componentInstance; + viewport = testComponent.viewport; + }); + + it('should emit on viewChange inside the Angular zone', fakeAsync(() => { + const zoneTest = jasmine.createSpy('zone test'); + testComponent.virtualForOf.viewChange.subscribe(() => zoneTest(NgZone.isInAngularZone())); + finishInit(fixture); + expect(zoneTest).toHaveBeenCalledWith(true); + })); + + describe('viewChange change detection behavior', () => { + let appRef: ApplicationRef; + + beforeEach(inject([ApplicationRef], (ar: ApplicationRef) => { + appRef = ar; + })); + + it('should run change detection if there are any viewChange listeners', fakeAsync(() => { + testComponent.virtualForOf.viewChange.subscribe(); + finishInit(fixture); + testComponent.items = Array(10).fill(0); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + + spyOn(appRef, 'tick'); + + viewport.scrollToIndex(5); + triggerScroll(viewport); + + expect(appRef.tick).toHaveBeenCalledTimes(1); + })); + }); + }); +}); + +@Component({ + template: ` + +
+ {{i}} - {{item}} +
+
+ `, + styles: ` + .cdk-virtual-scroll-content-wrapper { + display: flex; + flex-direction: column; + } + + .cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper { + flex-direction: row; + } + + .cdk-virtual-scroll-viewport { + background-color: #f5f5f5; + } + + .item { + box-sizing: border-box; + border: 1px dashed #ccc; + } + + .has-margin .item { + margin-bottom: 10px; + } + `, + encapsulation: ViewEncapsulation.None, + standalone: true, + imports: [ScrollingModule], +}) +class FixedSizeVirtualScroll { + @ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport; + // Casting virtualForOf as any so we can spy on private methods + @ViewChild(CdkVirtualForOf, {static: true}) virtualForOf: any; + + orientation = 'vertical'; + viewportSize = 200; + viewportCrossSize = 100; + itemSize = 50; + minBufferPx = 0; + maxBufferPx = 0; + items = Array(10) + .fill(0) + .map((_, i) => i); + trackBy: TrackByFunction; + templateCacheSize = 20; + + scrolledToIndex = 0; + hasMargin = false; + + get viewportWidth() { + return this.orientation == 'horizontal' ? this.viewportSize : this.viewportCrossSize; + } + + get viewportHeight() { + return this.orientation == 'horizontal' ? this.viewportCrossSize : this.viewportSize; + } +} + +/** Finish initializing the virtual scroll component at the beginning of a test. */ +function finishInit(fixture: ComponentFixture) { + // On the first cycle we render and measure the viewport. + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + + // On the second cycle we render the items. + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + + // Flush the initial fake scroll event. + animationFrameScheduler.flush(); + flush(); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); +} + +/** Trigger a scroll event on the viewport (optionally setting a new scroll offset). */ +function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) { + if (offset !== undefined) { + viewport.scrollToOffset(offset); + } + dispatchFakeEvent(viewport.scrollable.getElementRef().nativeElement, 'scroll'); + animationFrameScheduler.flush(); +}