From a6290b6cf99e18fe722efc959bdf00a33818bfdb Mon Sep 17 00:00:00 2001 From: Martin Dragnev Date: Tue, 21 Jul 2026 16:23:11 +0300 Subject: [PATCH 1/4] test(carousel): add zoneless tests for slide activation and dynamic slide changes --- .../src/carousel/carousel.component.spec.ts | 87 ++++++++++++++++++- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/projects/igniteui-angular/carousel/src/carousel/carousel.component.spec.ts b/projects/igniteui-angular/carousel/src/carousel/carousel.component.spec.ts index ba4e8bfda8b..117e2d0cade 100644 --- a/projects/igniteui-angular/carousel/src/carousel/carousel.component.spec.ts +++ b/projects/igniteui-angular/carousel/src/carousel/carousel.component.spec.ts @@ -1,4 +1,4 @@ -import { Component, ViewChild, TemplateRef, ChangeDetectionStrategy, ElementRef } from '@angular/core'; +import { Component, ViewChild, TemplateRef, ChangeDetectionStrategy, ElementRef, provideZonelessChangeDetection, inject, ChangeDetectorRef } from '@angular/core'; import { TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { @@ -1049,6 +1049,46 @@ describe('Carousel', () => { }); }); +describe('Carousel Zoneless Tests:', () => { + let mockElement: any; + let mockElementRef: ElementRef; + + beforeEach(async () => { + mockElement = document.createElement("div"); + mockElementRef = new ElementRef(mockElement); + await TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + CarouselDynamicSlidesWithNoActiveComponent, + ], + providers: [ + { provide: ElementRef, useValue: mockElementRef }, + IgxSlideComponent, + provideZonelessChangeDetection() + ] + }).compileComponents(); + }); + + it('should activate the correct slide when the entire collection is replaced from the last slide', async () => { + const fix = TestBed.createComponent(CarouselDynamicSlidesWithNoActiveComponent); + await wait(16); + fix.detectChanges(); + await fix.whenStable(); + const car: IgxCarouselComponent = fix.componentInstance.carousel; + HelperTestFunctions.verifyActiveSlide(car, 0); + + // Replace the entire slide collection; + fix.componentInstance.changeSlides(); + await wait(16); + fix.detectChanges(); + await fix.whenStable(); + + expect(car.total).toEqual(3); + // the carousel should activate the first slide again + HelperTestFunctions.verifyActiveSlide(car, 0); + }); +}); + class HelperTestFunctions { public static NEXT_BUTTON_CLASS = '.igx-carousel__arrow--next'; public static PRIV_BUTTON_CLASS = '.igx-carousel__arrow--prev'; @@ -1117,7 +1157,7 @@ class HelperTestFunctions { const carouselElement = fixture.debugElement.query(By.css('igx-carousel')); const touchManager = carouselElement.injector.get(HammerGesturesManager); const hammerManager = touchManager.getManagerForElement(carouselElement.nativeElement); - (hammerManager as any).emit('tap', { target: activeSlide, srcEvent: { preventDefault: () => {} } }); + (hammerManager as any).emit('tap', { target: activeSlide, srcEvent: { preventDefault: () => { } } }); } public static simulatePan(fixture, carousel, deltaOffset, velocity, dir: 'horizontal' | 'vertical') { @@ -1139,8 +1179,8 @@ class HelperTestFunctions { deltaY, duration: 100, velocity, - preventDefault: ( () => { }), - srcEvent: { preventDefault: () => {} } + preventDefault: (() => { }), + srcEvent: { preventDefault: () => { } } }; (hammerManager as any).emit(event, panOptions); @@ -1298,3 +1338,42 @@ class CarouselDynamicSlidesComponent { ); } } + +@Component({ + template: ` + + @for (slide of slides; track slide) { + +

{{slide.text}}

+
+ } +
+ `, + changeDetection: ChangeDetectionStrategy.Eager, + imports: [IgxCarouselComponent, IgxSlideComponent] +}) +class CarouselDynamicSlidesWithNoActiveComponent { + @ViewChild('carousel', { static: true }) public carousel: IgxCarouselComponent; + + public loop = true; + public slides = []; + private cdr = inject(ChangeDetectorRef); + + constructor() { + this.slides.push( + { text: 'Slide 1' }, + { text: 'Slide 2' }, + { text: 'Slide 3' }, + { text: 'Slide 4' } + ); + } + + public changeSlides() { + this.slides = [ + { text: 'New Slide 1' }, + { text: 'New Slide 2' }, + { text: 'New Slide 3' } + ]; + this.cdr.markForCheck(); + } +} From 5a297d8e7b0c2036d48fbeb8d60b690fff2f990e Mon Sep 17 00:00:00 2001 From: Martin Dragnev Date: Tue, 21 Jul 2026 16:32:29 +0300 Subject: [PATCH 2/4] fix(carousel): improve slide fallback logic and trigger change detection when changing collections --- .../carousel/src/carousel/carousel.component.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/projects/igniteui-angular/carousel/src/carousel/carousel.component.ts b/projects/igniteui-angular/carousel/src/carousel/carousel.component.ts index d13cde0aae6..82de313c307 100644 --- a/projects/igniteui-angular/carousel/src/carousel/carousel.component.ts +++ b/projects/igniteui-angular/carousel/src/carousel/carousel.component.ts @@ -2,7 +2,7 @@ import { NgClass, NgTemplateOutlet } from '@angular/common'; import { AfterContentInit, Component, ContentChild, ContentChildren, ElementRef, EventEmitter, HostBinding, HostListener, Input, IterableChangeRecord, IterableDiffer, IterableDiffers, OnDestroy, Output, QueryList, TemplateRef, ViewChild, ViewChildren, booleanAttribute, inject, ChangeDetectionStrategy } from '@angular/core'; import { merge, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { CarouselResourceStringsEN, ICarouselResourceStrings, isLeftToRight} from 'igniteui-angular/core'; +import { CarouselResourceStringsEN, ICarouselResourceStrings, isLeftToRight } from 'igniteui-angular/core'; import { first, IBaseEventArgs, last, PlatformUtil } from 'igniteui-angular/core'; import { CarouselAnimationDirection, IgxCarouselComponentBase } from './carousel-base'; import { IgxCarouselIndicatorDirective, IgxCarouselNextButtonDirective, IgxCarouselPrevButtonDirective } from './carousel.directives'; @@ -90,9 +90,9 @@ export class IgxCarouselComponent extends IgxCarouselComponentBase implements On /** @hidden */ @HostBinding('class.igx-carousel--vertical') - public get isVertical(): boolean { - return this.vertical; - } + public get isVertical(): boolean { + return this.vertical; + } /** * Returns the class of the carousel component. @@ -1071,7 +1071,9 @@ export class IgxCarouselComponent extends IgxCarouselComponentBase implements On this.slideRemoved.emit({ carousel: this, slide }); if (slide.active) { slide.active = false; - this.currentItem = this.get(slide.index < this.total ? slide.index : this.total - 1); + if (this.currentItem === slide) { // ← only fall back if nothing better was found + this.currentItem = this.get(slide.index < this.total ? slide.index : this.total - 1); + } } }); @@ -1090,6 +1092,7 @@ export class IgxCarouselComponent extends IgxCarouselComponentBase implements On this.slides.first.active = true; } this.play(); + this.cdr.markForCheck(); }); } } From 73335e7fdba966a44b05e00506249df653c671f1 Mon Sep 17 00:00:00 2001 From: Martin Dragnev Date: Tue, 21 Jul 2026 17:02:44 +0300 Subject: [PATCH 3/4] chore(carousel): reset testing module before each zoneless test and update slide activation test description --- .../carousel/src/carousel/carousel.component.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/igniteui-angular/carousel/src/carousel/carousel.component.spec.ts b/projects/igniteui-angular/carousel/src/carousel/carousel.component.spec.ts index 117e2d0cade..b219e8f24d4 100644 --- a/projects/igniteui-angular/carousel/src/carousel/carousel.component.spec.ts +++ b/projects/igniteui-angular/carousel/src/carousel/carousel.component.spec.ts @@ -1054,6 +1054,7 @@ describe('Carousel Zoneless Tests:', () => { let mockElementRef: ElementRef; beforeEach(async () => { + TestBed.resetTestingModule(); mockElement = document.createElement("div"); mockElementRef = new ElementRef(mockElement); await TestBed.configureTestingModule({ @@ -1069,7 +1070,7 @@ describe('Carousel Zoneless Tests:', () => { }).compileComponents(); }); - it('should activate the correct slide when the entire collection is replaced from the last slide', async () => { + it('should activate and show the correct slide when the entire collection is replaced', async () => { const fix = TestBed.createComponent(CarouselDynamicSlidesWithNoActiveComponent); await wait(16); fix.detectChanges(); From 1db5c6d99a923682d49e593b5be992b048d47db8 Mon Sep 17 00:00:00 2001 From: Martin Dragnev <37667452+mddragnev@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:32:04 +0300 Subject: [PATCH 4/4] chore(*): update code comment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../carousel/src/carousel/carousel.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/carousel/src/carousel/carousel.component.ts b/projects/igniteui-angular/carousel/src/carousel/carousel.component.ts index 82de313c307..ad296e33c29 100644 --- a/projects/igniteui-angular/carousel/src/carousel/carousel.component.ts +++ b/projects/igniteui-angular/carousel/src/carousel/carousel.component.ts @@ -1071,7 +1071,7 @@ export class IgxCarouselComponent extends IgxCarouselComponentBase implements On this.slideRemoved.emit({ carousel: this, slide }); if (slide.active) { slide.active = false; - if (this.currentItem === slide) { // ← only fall back if nothing better was found + if (this.currentItem === slide) { // Only fall back if nothing better was found. this.currentItem = this.get(slide.index < this.total ? slide.index : this.total - 1); } }