angulartestingjasminekarma-jasmineangular-directive

How to provide different test cases by use of one test component for a directive which changes the element stlye in ngOnInit() in Angular 19?


I have a directive which looks like the following:

import { Directive, ElementRef, Input, Renderer2, OnInit } from '@angular/core';

@Directive({
  selector: '[appChangeStyle]'  
})
export class ChangeStyleDirective implements OnInit {
  @Input() appChangeStyle: string = '';  

  constructor(private el: ElementRef, private renderer: Renderer2) {}

  ngOnInit(): void {
    this.changeBackgroundColor(this.appChangeStyle);
  }

  private changeBackgroundColor(color: string): void {
    this.renderer.setStyle(this.el.nativeElement, 'background-color', color);
  }
}

I would like to test this directive with different test scenarios in Angular, similar to the following code:

import { ChangeStyleDirective } from './change-style.directive';
import { ElementRef, Renderer2 } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';

@Component({
  template: `<div appChangeStyle="{{color}}"></div>`
})
class TestComponent {
  color = 'red';
}

describe('ChangeStyleDirective', () => {
  let fixture: ComponentFixture<TestComponent>;
  let directiveEl: HTMLElement;
  let directive: ChangeStyleDirective;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ChangeStyleDirective, TestComponent],
    });

    fixture = TestBed.createComponent(TestComponent);
    directiveEl = fixture.nativeElement.querySelector('div');
    fixture.detectChanges();  

    directive = new ChangeStyleDirective(directiveEl as ElementRef, TestBed.inject(Renderer2));
  });

  it('should create an instance of ChangeStyleDirective', () => {
    expect(directive).toBeTruthy();
  });

  it('should apply the correct background color based on input', () => {
    // Initial color is 'red'
    fixture.componentInstance.color = 'red';
    fixture.detectChanges(); 
    expect(directiveEl.style.backgroundColor).toBe('red');

    // Change color to 'blue'
    fixture.componentInstance.color = 'blue';
    fixture.detectChanges();  
    expect(directiveEl.style.backgroundColor).toBe('blue');
  });
});

However the tests are not successful because directive ngOnInit is called only once. Is there a way to solve this issue? thank you.


Solution

  • Solution with ngOnInit:

    We take a viewChild to easily access the directive.

    We recreate the component so that the ngOnInit of the directive retriggers and then the test case will pass.

    The rest of the approach is explained in the below answer.

    import { ElementRef, Input, Renderer2, ViewChild } from '@angular/core';
    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { Component } from '@angular/core';
    import { ChangeStyleDirective } from './app.component';
    
    @Component({
      template: `<div [appChangeStyle]="color"></div>`,
    })
    class TestComponent {
      @ViewChild(ChangeStyleDirective) appChangeStyle;
      @Input() color = 'red';
    }
    
    describe('ChangeStyleDirective', () => {
      let fixture: ComponentFixture<TestComponent>;
      let directiveEl: HTMLElement;
      let directive: ChangeStyleDirective;
      let component: TestComponent;
    
      beforeEach(() => {
        TestBed.configureTestingModule({
          declarations: [ChangeStyleDirective, TestComponent],
        });
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        directiveEl = fixture.nativeElement.querySelector('div');
        fixture.detectChanges();
        directive = component.appChangeStyle;
      });
    
      it('should create an instance of ChangeStyleDirective', () => {
        expect(directive).toBeTruthy();
      });
    
      it('should apply the correct background color based on input', () => {
        // Initial color is 'red'
        fixture.componentRef.setInput('color', 'red');
        fixture.detectChanges();
        expect(directiveEl.style.backgroundColor).toBe('red');
    
        // Change color to 'blue'
        fixture = TestBed.createComponent(TestComponent);
        fixture.componentRef.setInput('color', 'blue');
        directiveEl = fixture.nativeElement.querySelector('div');
        fixture.detectChanges();
        expect(directiveEl.style.backgroundColor).toBe('blue');
      });
    });
    

    Solution with ngOnChanges:

    It is better for your component to react to @Input changes, so set it on ngOnChanges (Which fires on every change of @Inputs):

    import { Directive, ElementRef, Input, Renderer2, OnInit } from '@angular/core';
    
    @Directive({
      selector: '[appChangeStyle]'  
    })
    export class ChangeStyleDirective implements OnInit {
      @Input() appChangeStyle: string = '';  
    
      constructor(private el: ElementRef, private renderer: Renderer2) {}
    
      ngOnChanges(): void {
        this.changeBackgroundColor(this.appChangeStyle);
      }
    
      private changeBackgroundColor(color: string): void {
        this.renderer.setStyle(this.el.nativeElement, 'background-color', color);
      }
    }
    

    First set the color property to an @Input:

    @Component({
      template: `<div [appChangeStyle]="color"></div>`
    })
    class TestComponent {
      @Input() color = 'red';
    }
    

    Then use the setInput method to dynamically change it.

      it('should apply the correct background color based on input', () => {
        // Initial color is 'red'
        fixture.componentRef.setInput('color', 'red');
        fixture.detectChanges(); 
        expect(directiveEl.style.backgroundColor).toBe('red');
    
        // Change color to 'blue'
        fixture.componentRef.setInput('color', 'red');
        fixture.detectChanges();  
        expect(directiveEl.style.backgroundColor).toBe('blue');
      });