如何从Jasmine测试Angular 2/4中触发文档级事件

10
根据 Angular Testing 文档,要在测试中触发事件,我们使用调试元素上的 triggerEventHandler() 方法。此方法需要传递事件名称和对象。如果我们使用 HostListener 添加事件,则该方法可行。例如:@HostListener('mousemove', ['$event']),或者添加 document 级别事件,则可以像这样做:@HostListener('document:mousemove', ['$event'])。
在我的当前指令实现中,由于无法嵌套 HostListeners,我使用 document.addEventListener 在 HostListener 中添加 document 级别事件。
代码如下:
@HostListener('mousedown', ['$event'])
callMouseDown(event){
  if (something) {
   document.addEventListener('mousemove', this.callMouseMove.bind(this));
  }
}

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

现在,我想从我的测试中触发添加在document级别的mousemove事件。目前triggerEventHandler()的实现不起作用,即测试中没有触发监听器。

我该如何使其工作?有人可以给我一些指导吗?

编辑:添加测试:

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});
}));

这个 this.callMouseMove() 的语法正确吗? - yurzui
document.dispatchEvent(new Event("mousemove")) 会触发事件。 - yurzui
@yurzui 我该如何在这里传递对象? - Angela Roux
2
const event = new Event("mousemove"); event.pageX = 250; document.dispatchEvent(event) - yurzui
@yurzui 但我非常好奇,是否有更优雅的方式可以一行代码实现?使用 CustomEvent? - ShellZero
显示剩余4条评论
2个回答

9

看起来你所描述的类似问题已经在angular仓库中记录为此处的问题。

你可以通过组件实例获取指令并访问方法(在你的情况下,它是callMouseMove)。 以下方式应该有效:

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});
}));

希望这可以帮到你。

更新

我意识到,如果您的指令中有私有方法,则我的方法无法正常工作。更好的方法是通过编程创建自定义事件或鼠标事件,并在文档级别或元素本身上触发事件。这种方法由yurzui提到。感谢他。

以下是如何操作:

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

您可以在规范中调用此函数:
programmaticTrigger("mousemove", {clientX: 250, clientY: 320});

请注意,此处我没有传递 pageXpageY,因为它们是只读的,但它们是根据 clientXclientY 值在内部计算出来的。
如果你想创建一个自定义事件并传入任何值,可以按照以下步骤进行:
function programmaticTrigger(eventType: string, evtObj: any) {
       let evt: any;
       evt = Event(eventType, evtObj);
       document.dispatchEvent(evt);
    }

按照以下方式调用:

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

希望这有所帮助。

0

我有一个替代方案来解决这个问题。如果你想知道为什么ShellZero的答案对我不起作用,请看下面。

我的解决方案是创建一个可注入的“包装器”服务,可以返回document。这样,在生产中它会像往常一样获取文档,但在测试中,我可以模拟包装器并提供自己的“文档”并向其分派事件。

document-wrapper.service.ts

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

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

  constructor() { }

  public getDocument() {
    return document;
  }
}


在我的组件中,我可以在构造函数中注入此服务并从中获取document。在我的情况下,我在ngOnInit方法中使用它。

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
    });

}

这需要在测试中进行一些额外的工作。我必须制作一个模拟包装器并注入它。默认情况下,我的模拟返回document,以便现有的测试不会出错。

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();
  }));

然后,在我的spec方法中,我正在测试事件处理,我必须设置我的模拟以返回一个不同的元素,而不是document。我找到的最好的方法是使用组件本身最外层的div。因为我的调用addEventListenerngOnInit中,所以我还必须再次调用ngOnInit。一旦我完成了这个步骤,我就可以自由地分派事件并制定我的期望。
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
});

虽然ShellZero的回答是我能找到的最好的,但它并没有让我满意。在测试Angular组件上的事件处理程序时,我认为仅调用组件本身的处理程序方法是不足够的,因为这并不能证明组件已经正确地订阅了该事件。我更喜欢触发事件并期望组件正确地响应。

ShellZero回答中的“更新”部分在我实现时根本无法工作。我认为这是因为Karma将Angular组件放在一个没有访问根文档的iFrame中。如果我错了,我很想知道。

我不喜欢我的解决方案的唯一一件事是它添加了生产代码,这些代码仅在测试时才需要。通常情况下,我会更愿意在测试中跳过很多麻烦来避免为了测试而更改生产代码。在这种情况下,我看不到其他的解决办法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接