Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 686
Fotios Tragopoulos Avatar asked Apr 30 '19 19:04

Fotios Tragopoulos


People also ask

What is the correct way to trigger a click event on a button when testing an Angular component?

For click event we can use triggerEventHandler method of Angular DebugElement class. We can also call native JavaScript click method of button. On click of button, we call a component method and it is possible that our component method has other dependencies to execute.

Does fixture detectChanges call ngOnInit?

fixture. detectChanges() tells Angular to run change-detection. Finally! Every time it is called, it updates data bindings like ng-if, and re-renders the component based on the updated data. Calling this function will cause ngOnInit to run only the first time it is called.

What is TestBed in Angular?

TestBed is the primary api for writing unit tests for Angular applications and libraries.


1 Answers

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();
  });
});
like image 180
Shashank Vivek Avatar answered Sep 17 '22 16:09

Shashank Vivek