Search code examples
angularangular-templateangular-test

Selecting a specific TemplateRef from Angular component test?


I have the following component that has links to open a modal, which I can vary the content of by passing a template reference into.

<a id="spec-tos" (click)="openModal($event, termsOfServiceModalTemplate)">Terms of Service</a>
<a id="spec-pp" (click)="openModal($event, privacyPolicyModalTemplate)">Privacy</a>

<ng-template #termsOfServiceModalTemplate>
  terms of service text goes here...
</ng-template>

<ng-template #privacyPolicyModalTemplate>
  privacy policy text goes here...
</ng-template>
export class FooterComponent {
  modalRef: undefined | BsModalRef;

  constructor(private modalService: BsModalService) {}

  openModal($event: Event, template: TemplateRef<NgTemplateOutlet>): void {
    $event.preventDefault();
    this.modalRef = this.modalService.show(template, {
      class: 'modal-dialog-scrollable'
    });
  }
}

How can I test that the links open the correct template? I need to be able to select the templateRef ID from my test, but I'm not sure how to do this. here's my test

it('should call the openModal() method when clicking on the privacy policy link', () => {
    spyOn(component, 'openModal');
    const link = debugEl.query(By.css('#spec-pp'));
    //This next line is wrong and does not work
    const tmpl = debugEl.query(By.directive(TemplateRef));
    link.triggerEventHandler('click', null);
    expect(component.openModal).toHaveBeenCalledWith(new MouseEvent('click'), tmpl);
});

I know that debugEl.query(By.directive(TemplateRef)) does not work... but that's just to give an idea of what I want to do here. How do I select a specific template from the component I'm testing?


Edit: The answer from @neo below is the solution, but I was able to take his solution and package it up into a re-usable helper function

/**
 * Used for detecting an `<ng-template>` element reference that was passed to a function, typically for modals
 * We don't have access to much except the name of the template and a number that it maps to internally.
 * If we can return the number, then we've found the template by name, which is all we really want to do here
 * //https://stackoverflow.com/a/55432163/79677
 */
export const templateExists = (template: TemplateRef<NgTemplateOutlet>, templateName: string): boolean => {
    // tslint:disable-next-line:no-any  --  There is no other way to get this object, it doesn't exist in the typedefs!
    const refs = (template as any)._def.references as {[templateName: string]: number};
    //If we have a number, then we've found the template by name
    return !isNaN(refs[templateName]);
};

Use it like this:

it('should call the openModal() method when clicking on the privacy policy link', () => {
    const modalSpy = spyOn(component, 'openModal');
    const link = debugEl.query(By.css('.footer-nav .spec-privacy'));
    link.triggerEventHandler('click', null);
    expect(component.openModal).toHaveBeenCalled();
    expect(templateExists(modalSpy.calls.mostRecent().args[1], 'privacyPolicyModalTemplate')).toEqual(true);
});

Solution

  • Sorry for the delay, been busy these days. You should modify your spec.ts as follows.

    import { TestBed, async, ComponentFixture } from '@angular/core/testing';
    import { FooterComponent } from './footer.component';
    import { BrowserModule, By } from '@angular/platform-browser';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    import { MatButtonModule } from '@angular/material';
    import { ModalModule } from 'ngx-bootstrap';
    
    describe('FooterComponent ', () => {
      let component: FooterComponent;
      let fixture: ComponentFixture<FooterComponent>;
      beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [
            FooterComponent
          ],
          imports: [
            // your imports here
            ModalModule.forRoot()
          ]
        }).compileComponents();
      }));
    
      beforeEach(() => {
        fixture = TestBed.createComponent(FooterComponent);
        component = fixture.debugElement.componentInstance;
        fixture.detectChanges();
      });
    
      it('should create the app', () => {
        expect(component).toBeTruthy();
      });
    
    
      it('should call the openModal() method when clicking on the links', () => {
        const obj = spyOn(component, 'openModal');
        let link = fixture.debugElement.query(By.css('#spec-tos'));
        link.triggerEventHandler('click', null);
        expect(component.openModal).toHaveBeenCalled();
        expect(obj.calls.mostRecent().args[1]._def.references.termsOfServiceModalTemplate).toBeDefined();
    
        link = fixture.debugElement.query(By.css('#spec-pp'));
        link.triggerEventHandler('click', null);
        expect(component.openModal).toHaveBeenCalledTimes(2);
        expect(obj.calls.mostRecent().args[1]._def.references.privacyPolicyModalTemplate).toBeDefined();
      });
    });
    
    

    Enjoy!!