Search code examples
unit-testingkarma-jasmineangular7

How can I test a modal's elements with ng-template and the action that triggers it?


I read the Angular Testing and I'm not sure if there is any reference about testing elements inside a modal and how to check custom actions. My purpose is to write the necessary tests show I will be sure that my function and the modal works as expected.

As the modal is hidden, the tests to check if the elements of the modal appear, fail. So I suppose that there is something missing here.

This is my photos.components.ts file:

import {Component, OnInit, ViewEncapsulation} from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';

@Component({
  selector: 'app-photos',
  templateUrl: './photos.component.html',
  styleUrls: ['./photos.component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class PhotosComponent implements OnInit {

  constructor(private modalService: NgbModal) { }

  openDarkModal(content) {
    this.modalService.open(content, { windowClass: 'dark-modal', size: 'lg', centered: true });
  }

  ngOnInit() {
  }

}

This is my photos.component.html file:

<div>
  <div class="col-lg-4 col-sm-6 mb-3">
    <a><img (click)="openDarkModal(content)" id="photo-one" class="img-fluid z-depth-4 relative waves-light" src="#" alt="Image" data-toggle="content" data-target="#content"></a>
  </div>
</div>

<!-- Dark Modal -->
<ng-template #content let-modal id="ng-modal">
  <div class="modal-header dark-modal">
    <img (click)="modal.dismiss('Cross click')" id="modal-image" class="embed-responsive-item img-fluid" src="#" alt="Image" allowfullscreen>
  </div>
    <div class="justify-content-center flex-column flex-md-row list-inline">
      <ul class="list-inline flex-center text-align-center text-decoration-none" id="modal-buttons-list">
        <li><a style="color: white;" href="#"><button mdbBtn type="button" size="sm" class="waves-light" color="indigo" mdbWavesEffect><i class="fab fa-facebook-f"></i></button></a></li>
        <li><a style="color: white;" href="#"><button mdbBtn type="button" size="sm" class="waves-light" color="cyan" mdbWavesEffect><i class="fab fa-twitter"></i></button></a></li>
        <li><a style="color: white;" href="#"><button mdbBtn type="button" size="sm" class="waves-light btn btn-blue-grey" mdbWavesEffect><i class="fas fa-envelope"></i></button></a></li>
      </ul>
    </div>
</ng-template>

and this is where I am with the photos.component.spec.ts file:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PhotosComponent } from './photos.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';

describe('PhotosComponent', () => {
  let component: PhotosComponent;
  let fixture: ComponentFixture<PhotosComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ PhotosComponent ],
      schemas: [NO_ERRORS_SCHEMA]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(PhotosComponent);
    component = fixture.componentInstance;
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should render the first photo', () => {
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('#photo-one')).toBeTruthy();
  });
});

I need the test cases for the elements inside the Dark Modal and the test for the openDarkModal. Except for the code, a reference in Angular 7 testing for beginners would be appreciated.


Solution

  • Let me help you with this one. Lets say you have

    app.component.html

    <div id="title">
        {{title}}
    </div>
    <ng-template #content
                 let-modal
                 id="ng-modal">
      <div class="modal-header dark-modal">
        Header
      </div>
      <div class="justify-content-center flex-column flex-md-row list-inline">
        Body
      </div>
    </ng-template>
    

    app.component.ts

    import { Component, ViewChild, TemplateRef } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css'],
    })
    export class AppComponent {
      title = 'AngularProj';
      @ViewChild('content') modalRef: TemplateRef<any>;
    }
    

    You need to write spec file with slightly different way:

    app.component.spec.ts

    import { TestBed, async, ComponentFixture } from '@angular/core/testing';
    import { AppComponent } from './app.component';
    import { ViewChild, Component, OnInit, AfterContentInit, TemplateRef } from '@angular/core';
    import { By } from '@angular/platform-browser';
    
    @Component({
      template: `
        <ng-container *ngTemplateOutlet="modal"> </ng-container>
        <app-root></app-root>
      `,
    })
    class WrapperComponent implements AfterContentInit {
      @ViewChild(AppComponent) appComponentRef: AppComponent;
      modal: TemplateRef<any>;
      ngAfterContentInit() {
        this.modal = this.appComponentRef.modalRef;
      }
    }
    
    describe('AppComponent', () => {
      let app: AppComponent;
      let fixture: ComponentFixture<WrapperComponent>;
      beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [WrapperComponent, AppComponent],
        }).compileComponents();
      }));
      beforeEach(() => {
        fixture = TestBed.createComponent(WrapperComponent);
        const wrapperComponent = fixture.debugElement.componentInstance;
        app = wrapperComponent.appComponentRef;
        fixture.detectChanges();
      });
      it('should create the app', async(() => {
        expect(app).toBeDefined();
      }));
      it('should have title in HtmL ', () => {
        const titleText = (fixture.debugElement.nativeElement.querySelector('#title').innerText);
        expect(titleText).toBe('AngularProj');
      });
      it('should have Header in HtmL ', () => {
        const headerText = (fixture.debugElement.queryAll(By.css('.modal-header.dark-modal'))[0].nativeElement.innerText);
        expect(headerText).toBe('Header');
      });
    });
    

    1. As you can see, I wrapped the app-root with a sample testing component (WrapperComponent).
    2. Since, app-root has ng-template, so it won't render on it's own. This creates a tricky situation as we need to render this part of the app.component.
    3. Expose ng-template by creating @ViewChild('content') modalRef: TemplateRef<any>; and then using it to render inside WrapperComponent .

    I know it seems like a hack but over all the articles I went through, that's how we can achieve this.


    For testing something like:

    openDarkModal(content) {
        this.modalService.open(content, { windowClass: 'dark-modal', size: 'lg', centered: true });
     }
    

    you can use spy , but before that make modalService public so that it can be spied upon:

    constructor(public modalService: NgbModal) { }
    

    You can also use jasmine.createSpyObj and keep the service private.

    and then in spec:

       import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
    
       TestBed.configureTestingModule({ 
          imports: [NgbModalModule], 
          declarations: [PhotosComponent, /*WrapperComponent*/], 
          schemas: [NO_ERRORS_SCHEMA], 
        })
    
    // and in it block
      it('should call modal Service open function when clicked ', async(() => {
        spyOn(component.modalService,'open').and.callThrough();
        const openModalEle= fixture.debugElement.nativeElement.querySelector('#photo-one'));
         openModalEle.click();
         expect(component.modalService.open).toHaveBeenCalled();    
      }));
    

    Update:

    with Angular 11, there are few changes which you will require:

    @Component({
      template: `
        <div>
          <ng-container *ngTemplateOutlet="modal"> </ng-container>
        </div>
        <app-root> </app-root> 
      `,
    })
    class WrapperComponent implements AfterViewInit {
      @ViewChild(AppComponent) appComponentRef: AppComponent;
      modal: TemplateRef<any>;
      constructor(private cdr: ChangeDetectorRef) {}
      ngAfterViewInit() {
        this.modal = this.appComponentRef.modalRef;
        this.cdr.detectChanges();
      }
    }
    describe('AppComponent', () => {
      let fixture: ComponentFixture<WrapperComponent>;
      let wrapperComponent: WrapperComponent;
    
      beforeEach(
        waitForAsync(() => {
          TestBed.configureTestingModule({
            declarations: [WrapperComponent, AppComponent],
            imports: [ModalModule.forRoot()],
          }).compileComponents();
        })
      );
      beforeEach(() => {
        fixture = TestBed.createComponent(WrapperComponent);
        wrapperComponent = fixture.debugElement.componentInstance;
        fixture.detectChanges();
      });
      it('should create the app', () => {
        expect(wrapperComponent).toBeDefined();
        expect(wrapperComponent.appComponentRef).toBeDefined();
      });
    });