How to trigger document level events from a jasmine test Angular 2/4
Asked Answered
J

2

10

According to the Angular Testing documentation, to trigger the events from the tests, we use the triggerEventHandler() method on the debug element. This method takes the event name and the object. Now, this works if we are adding the events using the HostListener. For ex: @HostListener('mousemove', ['$event']) or to add a document level event, we do it something like this @HostListener('document:mousemove', ['$event']).

In my current directive implementation, as I cannot nest the HostListeners, I add the document level events using the document.addEventListener inside a HostListener.
Code is as follows:

@HostListener('mousedown', ['$event'])
callMouseDown(event){
  if (something) {
   document.addEventListener('mousemove', this.callMouseMove.bind(this));
  }
}

callMouseMove(event){
 // do some computations.
}

Now, I want to trigger the mousemove event which is added at the document level from my tests. The current implementation of triggerEventHandler() doesn't work, i.e., the listener is not fired in the test.

How can I get this to work? Could anyone help me with some guidance.

Edit: Added Test:

it('test spec', inject([MyService], (service: MyService) =>{
   x = [];
  //service calls the method
  service.evtEmit.subscribe(e => {
   x.push(e);
  });
  triggerEventHandler("mousedown", {pageX: 200, pageY: 300});
  triggerEventHandler("document:mousemove", {pageX: 250, pageY: 320});
  // the above statement won't work.
  expect(arr).toEqual({x:50, y: 20});
}));
Janettjanetta answered 22/8, 2017 at 18:19 Comment(9)
Is this this.callMouseMove() correct syntax?Icarian
Can you also add your test?Icarian
@Icarian Hi, I updated the test. and I made a mistake in the syntax of calling the function which I fixed it now.Janettjanetta
document.dispatchEvent(new Event("mousemove")) will trigger eventIcarian
@Icarian how do I pass the object here?Janettjanetta
const event = new Event("mousemove"); event.pageX = 250; document.dispatchEvent(event)Icarian
@Icarian I think your way of doing is the right way because my way doesn't work if I have a private method inside the directive. +1 for your answer. Maybe the user who posted would change their mind if you post it as an answer :)Prindle
@Icarian But I am very curious, if one can do this in more elegant way like in one single line? Using CustomEvent?Prindle
@Prindle hey, yes you can use CustomEvent as alternative. And yes, I think my way is the right way for testing such thingsIcarian
P
9

Seems like the similar problem which you have described is logged as the issue here on angular repo.

You can get the access of the directive with the component instance and can access the method (i.e., in your case it's the callMouseMove). The following way should work:

it('test spec', inject([MyService], (service: MyService) => {
   x = [];
  //service calls the method
  service.evtEmit.subscribe(e => {
   x.push(e);
  });
  triggerEventHandler("mousedown", {pageX: 200, pageY: 300});
  component.directive.callMouseMove({pageX: 250, pageY: 320});
  expect(arr).toEqual({x:50, y: 20});
}));

Hope this helps.

Update

I realized that my way doesn't work if you have private methods inside your directive. It is better to create custom events or in this case a mouse event programmatically and trigger the events either at document level or on the element itself. This method was mentioned by yurzui. Thanks to him.

Here's how you can do it:

function programmaticTrigger(eventType: string, evtObj: any) {
   let evt = new MouseEvent(eventType, evtObj);
   document.dispatchEvent(evt);
}

You can call this function from your spec as:

programmaticTrigger("mousemove", {clientX: 250, clientY: 320});

Note that, I am not passing pageX or pageY here in this case, because they are readonly but they are computed internally based on the clientX and clientY values.

If you want to create a custom event and pass in whatever values you want, you can do as follows:

function programmaticTrigger(eventType: string, evtObj: any) {
       let evt: any;
       evt = Event(eventType, evtObj);
       document.dispatchEvent(evt);
    }

Invoke it as follows:

programmaticTrigger("mousemove", {pageX: 250, pageY: 320});

Hope this helps.

Prindle answered 22/8, 2017 at 21:2 Comment(0)
H
0

I have an alternative solution to this problem. See below if you want to know why ShellZero's answer doesn't work for me.

My solution is to create an injectable "wrapper" service that can return document. That way, in production it gets the document as usual, but in tests, I can mock the wrapper and supply my own "document" and dispatch events to it.

document-wrapper.service.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class DocumentWrapperService {

  constructor() { }

  public getDocument() {
    return document;
  }
}

In my component, I can inject this service in the constructor and get document from it. In my case, I use it in the ngOnInit method.

some.component.ts

import { DocumentWrapperService } from './../../../services/document-wrapper/document-wrapper.service';
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'some-component',
  template: '<div id="container"><!-- Your HTML here --></div>',
  styleUrls: ['./some.component.css']
})
export class SomeComponent implements OnInit {
constructor(private documentWrapper: DocumentWrapperService) { }

  ngOnInit() {
    const _document = this.documentWrapper.getDocument();

    _document.addEventListener('mouseup', ($event: MouseEvent) => {
      // do something on mouse-up
    });

}

This requires a little bit of extra work in the test. I have to make a mock wrapper and inject it. By default, my mock returns document so existing tests don't break.

import { DocumentWrapperService } from './../../../services/document-wrapper/document-wrapper.service';
import * as td from 'testdouble';

describe('SomeComponent', () => {
  beforeEach(async(() => {
    // I use testdouble to make my mock, but you could roll your own or use some other library.
    mockDocumentWrapper = td.object(DocumentWrapperService.prototype);
    td.when(mockDocumentWrapper.getDocument()).thenReturn(document);

    TestBed.configureTestingModule({
      declarations: [SomeComponent],
      providers: [
        { provide: DocumentWrapperService, useValue: mockDocumentWrapper }
      ]
    })
      .compileComponents();
  }));

Then, in my spec method where I'm testing the event handling, I have to set up my mock to return a different element instead of document. The best thing I've found is to use the outermost div of the component itself. Because my call to addEventListener is in ngOnInit, I also have to call ngOnInit again. Once I've done that, I'm free to dispatch the event and make my expectations.

it("should do something when the user releases the mouse button", () => {
  const rootDivElement = fixture.nativeElement.querySelector("#container");
  td.when(mockDocumentWrapper.getDocument()).thenReturn(rootDivElement);
  component.ngOnInit();

  rootDivElement.dispatchEvent(new MouseEvent('mouseup', { clientY: 100, clientX: 200 }));

  // expectations go here
});

While ShellZero's answer is the best I could find, it wasn't satisfying to me. When testing an event handler on an Angular component, I don't think it's adequate to call the handler method on the component itself, because it doesn't prove that the component has subscribed to the correct event. I prefer to trigger the event and expect the component to react correctly.

The "Update" section in ShellZero's answer straight-up didn't work when I implemented it. I think it's because Karma puts the Angular components in an iFrame, which has no access to the root document. If that's wrong, I'd love to know.

The one thing I don't like about my solution is that it adds production code that is only required to make testing possible. I would normally prefer to jump through lots of hoops in my tests to avoid changing the production code for the sake of testing. In this case, I could not see a way to do that.

Holierthanthou answered 14/11, 2020 at 19:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.