1

I have an angular directive that attaches to an event on the element:

@Directive({
    selector: '[myDirective]'
})
export class MyDirective {
    @HostListener('click', ['$event']) click(event: Event): void {
        debugger;
        console.log(event); // <-- this is null in unit tests, MouseEvent when running the app normally
    }
}

This works fine, but for some reason the event parameter is null when unit testing the directive.

My Karma Jasmine unit test setup:

import { CommonModule } from '@angular/common';
import { Component, DebugElement, ElementRef, Injector } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

@Component({
    selector: 'host-component',
    template: `
        <input type="text" myDirective id="findMe"/>
    `
})
class HostComponent {
}

describe(`MyDirective`, () => {
    let host: HostComponent;
    let fixture: ComponentFixture<HostComponent>;

    let debugElement: DebugElement;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [
                CommonModule
            ],
            declarations: [
                HostComponent, MyDirective
            ]
        }).compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(HostComponent);
        host = fixture.componentInstance;

        debugElement = fixture.debugElement.query(By.css('#findMe'))

        fixture.autoDetectChanges();
    });

    describe(`should listen to the events`, () => {
        it(`should listen to the click event`, () => {
            fixture..triggerEventHandler('click', null);

            fixture.detectChanges();
            // expect...
        });
    });
});

Now, the problem: The directive is being hit in the unit test, but there is no event sent through as a parameter.

I've followed this example: https://codecraft.tv/courses/angular/unit-testing/directives/ but unfortunately it doesnt use the event parameter.

Edit

I've also followed this example to pass through arguments to the @HostListener() decorator:

@HostListener('mouseenter', ['$event.target.id']) 
    onMouseEnter(id: string) {
        // Logs the id of the element 
        // where the event is originally invoked.  
    console.log(id);
}

Edit 2

It seems that the events raised from the DebugElement does not really represent the actual event listeners from the DOM element?

From what Hojou said on this angular github issue, if you trigger the event from the nativeElement it works. So the following code does send through the event to the directive, just not too sure if its the right way:

describe(`should listen to the events`, () => {
    it(`should listen to the click event`, () => {
        // fixture.triggerEventHandler('click', null);
        debugElement.nativeElement.dispatchEvent(newEvent('click'));

        fixture.detectChanges();
        // expect...
    });
});

function newEvent(eventName: string) {
    const customEvent: CustomEvent<any> = document.createEvent('CustomEvent');  // MUST be 'CustomEvent'
    customEvent.initCustomEvent(eventName, false, false, null);
    return customEvent;
}
Johan Aspeling
  • 765
  • 1
  • 13
  • 38

3 Answers3

0

You are getting null there because you are passing null as an argument in

fixture..triggerEventHandler('click', null);

which I assume is a typo and should be

debugElement.triggerEventHandler('click', null);

if you pass an object there you will see it being logged in the directive

debugElement.triggerEventHandler('click', {test: 'test'});

I would add that personally I would do this test by 'executing' a click on the actual DOM object, so you don't need to specify / stub the event yourself, this seems to make for a more trustworthy test.

So you instead of the triggerEventHandler line you would do something like

debugElement.nativeElement.click()
gvdp
  • 86
  • 3
  • Did you try this? Its sending through null whatever you pass through there, or I might be missing something.. Either way, the directive should send through what is expected surely, as defined in the `@HostListener()`, which is the bound `'$event'` – Johan Aspeling Aug 21 '19 at 20:14
  • I'm still sending through `null` using `element.dispatchEvent` but that one is working... – Johan Aspeling Aug 21 '19 at 20:16
0

From the Edit 2, raising a custom event does the trick and sends through the attached element of the directive

From what Hojou said on this angular github issue (#22148, currently 'Open'), if you trigger the event from the nativeElement it works. So the following code does send through the event to the directive, just not too sure if its the right way:

describe(`should listen to the events`, () => {
    it(`should listen to the click event`, () => {
        // fixture.triggerEventHandler('click', null);
        debugElement.nativeElement.dispatchEvent(newEvent('click'));

        fixture.detectChanges();
        // expect...
    });
});

function newEvent(eventName: string) {
    const customEvent: CustomEvent<any> = document.createEvent('CustomEvent');  // MUST be 'CustomEvent'
    customEvent.initCustomEvent(eventName, false, false, null);
    return customEvent;
}
Johan Aspeling
  • 765
  • 1
  • 13
  • 38
0

You do not test directives directly. You test them via a test component. Here I am testing the *ngVar directive described here - How to declare a variable in a template in Angular

The main take away here is when testing a directive just use the exact same approach as when testing a component. And test the directive is behaving based on the test component doing what it should be doing!

import { NgVarDirective } from './ng-var.directive';
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DirectivesModule } from '../directives.module';
import { By } from '@angular/platform-browser';

@Component({
    template: '<ng-container *appNgVar="42 as myVar"><div>{{ myVar }}</div></ng-container>'
})
class TestComponent { }

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

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

        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

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

    it('should create an instance (HTMLElement)', () => {
        const el: HTMLElement = fixture.debugElement.nativeElement;
        const div: HTMLDivElement = el.querySelector('div');
        expect(div.textContent).toBe('42');
    });

    it('should create an instance (DebugElement)', () => {
        const el: DebugElement = fixture.debugElement;
        const de: DebugElement = el.query(By.css('div'));
        expect(de.nativeElement.textContent).toBe('42');
    });
});

Another example here (which is not testing a structural directive, unlike my example above) https://codecraft.tv/courses/angular/unit-testing/directives

Johan Aspeling
  • 765
  • 1
  • 13
  • 38
danday74
  • 52,471
  • 49
  • 232
  • 283