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.
this.callMouseMove()
correct syntax? – Icariandocument.dispatchEvent(new Event("mousemove"))
will trigger event – Icarianconst event = new Event("mousemove"); event.pageX = 250; document.dispatchEvent(event)
– Icarian