From a52a2afb6eda18b2ec1061def95deff02637d44c Mon Sep 17 00:00:00 2001 From: Jessica Janiuk Date: Thu, 2 Oct 2025 16:30:48 -0700 Subject: [PATCH] refactor(core): fix animations host binding tests (#64225) These tests were not properly validating against the host binding changes due to the fact that the styles were on the wrong components in some of the host binding cases. PR Close #64225 --- .../src/render3/instructions/animation.ts | 9 ++- .../core/test/acceptance/animation_spec.ts | 55 ++++++++++++++----- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/core/src/render3/instructions/animation.ts b/packages/core/src/render3/instructions/animation.ts index 432d63726bb..815cfcfb759 100644 --- a/packages/core/src/render3/instructions/animation.ts +++ b/packages/core/src/render3/instructions/animation.ts @@ -319,15 +319,18 @@ function animateLeaveClassRunner( cleanupFns.push(renderer.listen(el, 'animationend', handleOutAnimationEnd)); cleanupFns.push(renderer.listen(el, 'transitionend', handleOutAnimationEnd)); }); + trackLeavingNodes(tNode, el); + for (const item of classList) { renderer.addClass(el, item); } - // In the case that the classes added have no animations, we need to remove - // the element right away. This could happen because someone is intentionally - // preventing an animation via selector specificity. + ngZone.runOutsideAngular(() => { requestAnimationFrame(() => { + // In the case that the classes added have no animations, we need to remove + // the element right away. This could happen because someone is intentionally + // preventing an animation via selector specificity. determineLongestAnimation(el, longestAnimations, areAnimationSupported); if (!longestAnimations.has(el)) { clearLeavingNodes(tNode, el); diff --git a/packages/core/test/acceptance/animation_spec.ts b/packages/core/test/acceptance/animation_spec.ts index 32761bd2010..41b1f76cf87 100644 --- a/packages/core/test/acceptance/animation_spec.ts +++ b/packages/core/test/acceptance/animation_spec.ts @@ -374,7 +374,6 @@ describe('Animation', () => { it('should be host bindable', fakeAsync(() => { @Component({ selector: 'fade-cmp', - styles: styles, host: {'animate.leave': 'fade'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, @@ -383,6 +382,7 @@ describe('Animation', () => { @Component({ selector: 'test-cmp', + styles: styles, imports: [FadeComponent], template: '@if (show()) { }', encapsulation: ViewEncapsulation.None, @@ -400,6 +400,7 @@ describe('Animation', () => { expect(fixture.nativeElement.outerHTML).not.toContain('class="fade"'); cmp.show.set(false); fixture.detectChanges(); + tickAnimationFrames(1); expect(cmp.show()).toBeFalsy(); expect(fixture.nativeElement.outerHTML).toContain('class="fade"'); fixture.detectChanges(); @@ -413,7 +414,6 @@ describe('Animation', () => { it('should be host bindable with brackets', fakeAsync(() => { @Component({ selector: 'fade-cmp', - styles: styles, host: {'[animate.leave]': 'fade()'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, @@ -424,6 +424,7 @@ describe('Animation', () => { @Component({ selector: 'test-cmp', + styles: styles, imports: [FadeComponent], template: '@if (show()) { }', encapsulation: ViewEncapsulation.None, @@ -441,6 +442,7 @@ describe('Animation', () => { expect(fixture.nativeElement.outerHTML).not.toContain('class="fade"'); cmp.show.set(false); fixture.detectChanges(); + tickAnimationFrames(1); expect(cmp.show()).toBeFalsy(); expect(fixture.nativeElement.outerHTML).toContain('class="fade"'); fixture.detectChanges(); @@ -519,7 +521,6 @@ describe('Animation', () => { `; @Component({ selector: 'child-cmp', - styles: multiple, host: {'[animate.leave]': 'slide()'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, @@ -670,7 +671,6 @@ describe('Animation', () => { `; @Component({ selector: 'child-cmp', - styles: multiple, host: {'animate.leave': 'slide-out'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, @@ -1203,12 +1203,21 @@ describe('Animation', () => { it('should be host bindable', fakeAsync(() => { @Component({ - selector: 'test-cmp', - styles: styles, + selector: 'child-cmp', host: {'animate.enter': 'slide-in'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, }) + class ChildComponent {} + + @Component({ + selector: 'test-cmp', + styles: styles, + imports: [ChildComponent], + host: {'animate.enter': 'slide-in'}, + template: '', + encapsulation: ViewEncapsulation.None, + }) class TestComponent {} TestBed.configureTestingModule({animationsEnabled: true}); @@ -1227,15 +1236,23 @@ describe('Animation', () => { it('should be host bindable with brackets', fakeAsync(() => { @Component({ - selector: 'test-cmp', - styles: styles, + selector: 'child-cmp', host: {'[animate.enter]': 'slideIn()'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, }) - class TestComponent { + class ChildComponent { slideIn = signal('slide-in'); } + + @Component({ + selector: 'test-cmp', + styles: styles, + imports: [ChildComponent], + template: '', + encapsulation: ViewEncapsulation.None, + }) + class TestComponent {} TestBed.configureTestingModule({animationsEnabled: true}); const fixture = TestBed.createComponent(TestComponent); @@ -1254,12 +1271,26 @@ describe('Animation', () => { it('should be host bindable with events', fakeAsync(() => { const slideInCalled = jasmine.createSpy('slideInCalled'); @Component({ - selector: 'test-cmp', - styles: styles, + selector: 'child-cmp', host: {'(animate.enter)': 'slideIn($event)'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, }) + class ChildComponent { + slideIn(event: AnimationCallbackEvent) { + slideInCalled(); + event.target.classList.add('slide-in'); + event.animationComplete(); + } + } + + @Component({ + selector: 'test-cmp', + styles: styles, + imports: [ChildComponent], + template: '', + encapsulation: ViewEncapsulation.None, + }) class TestComponent { slideIn(event: AnimationCallbackEvent) { slideInCalled(); @@ -1277,7 +1308,6 @@ describe('Animation', () => { it('should compose class list when host binding and regular binding', fakeAsync(() => { @Component({ selector: 'child-cmp', - styles: styles, host: {'[animate.enter]': 'clazz'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, @@ -1320,7 +1350,6 @@ describe('Animation', () => { it('should compose class list when host binding a string and regular class strings', fakeAsync(() => { @Component({ selector: 'child-cmp', - styles: styles, host: {'animate.enter': 'slide-in'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None,