angular
April 5

Эмуляция событий в unit-тестах Angular

Существует два способа вызова событий в юнит-тестах. Давайте рассмотрим оба из них.

1. triggerEventHandler()

Экземпляр Angular DebugElement предоставляет удобный метод для вызова событий - triggerEventHandler(). Давайте посмотрим, как мы можем его использовать.

it('should set 😜 on click', () => {
  const fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();

  const h1 = fixture.debugElement.query(By.css('h1'));
  h1.triggerEventHandler('click', {});
  fixture.detectChanges();

  expect(
    fixture.debugElement.query(By.css('h1')).nativeElement.innerText
  ).toEqual('😜');
});
@Component({
  template: `
    <h1 (click)="onClick()">
      {{ emoji }}
    </h1>
  `
})
export class AppComponent {
  emoji: string;

  onClick() {
    this.emoji = '😜';
  }
}


У нас есть простой тест для компонента, который, при клике, устанавливает эмодзи. Мы используем метод query() для получения ссылки на элемент и вызываем обработчик события клика с помощью метода triggerEventHandler().

Три важных факта о методе triggerEventHandler():

- Он вызовет обработчик события только в том случае, если был объявлен на элементе с помощью привязок событий Angular, декораторов @HostListener() или @Output (а также меньше используемого Renderer.listen()). Например:

@Component({
  template: `
    <input (keydown.enter)="onEnter($event)">
    <my-component (data)="onData($event)"></my-component>
  `
})
export class AppComponent {
  emoji: string;

  onEnter() {
    this.emoji = '😜';
  }
  
  onData($event) {
    this.emoji = $event.emoji;
  }
}
it('should set the 😜 on (data)', () => {
  const fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();

  const myComponent = fixture.debugElement.query(By.directive(MyComponent));
  myComponent.triggerEventHandler('data', { emoji: '😜' });

  fixture.detectChanges();

  expect(
    fixture.debugElement.query(By.css('h1')).nativeElement.innerText
  ).toEqual('😜');
});

it('should set the 😜 on enter', () => {
  const fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();

  const input = fixture.debugElement.query(By.css('input'));
  input.triggerEventHandler('keydown.enter', {});
  fixture.detectChanges();

  expect(
    fixture.debugElement.query(By.css('h1')).nativeElement.innerText
  ).toEqual('😜');
});

Обратите внимание, что второй параметр представляет собой объект события, который будет передан обработчику.

- Он не вызовет автоматически changeDetection; мы должны вызвать его сами.

- В отличие от того, что думают или делают разработчики, нет необходимости использовать fakeAsync — Обработчик будет выполнен синхронно. Мы можем увидеть это при отладке кода:

2. Использование Javascript API

Давайте рассмотрим другие случаи, например, когда мы определяем события через API JavaScript. Например, давайте воспользуемся RxJS fromEvent:

import { untilDestroyed } from 'ngx-take-until-destroy';

@Component({
  template: `
    <h1 #h1>{{ emoji }}</h1>
  `
})
export class AppComponent {
  @ViewChild('h1') h1: ElementRef<HTMLElement>;

  emoji: string;

  ngAfterViewInit() {
    fromEvent(this.h1.nativeElement, 'click').pipe(
      untilDestroyed(this)
    ).subscribe(() => {
      this.emoji = '😜';
    });
  }
}

Теперь, как мы упоминали ранее, если мы используем метод triggerEventHandler()Angular, это не сработает, потому что Angular не знает об этом событии. К счастью, это событие клика, и мы можем программно вызвать клик, используя нативный метод click() элемента HTMLElement.

it('should set the 😜 on click', () => {
  const fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();

  const h1 = fixture.debugElement.query(By.css('h1'));
  h1.nativeElement.click(); <===

  fixture.detectChanges();
  expect(
    fixture.debugElement.query(By.css('h1')).nativeElement.innerText
  ).toEqual('😜');
});

Конечно, но что насчет других событий, таких как mouseenter, input и т. д.? Мы можем использовать API событий в сочетании с методом dispatchEvent(), чтобы запускать события. Например:

ngAfterViewInit() {
  fromEvent(this.h1.nativeElement, 'mouseenter').subscribe(() => {
    this.emoji = '😜';
  });
}
it('should set the 😜 on mouseenter', () => {
  const fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();

  const h1 = fixture.debugElement.query(By.css('h1'));
  const mouseenter = new MouseEvent('mouseenter');
  h1.nativeElement.dispatchEvent(mouseenter);

  fixture.detectChanges();
  expect(
    fixture.debugElement.query(By.css('h1')).nativeElement.innerText
  ).toEqual('😜');
});

Мы снова видим, что я не использую fakeAsync, и это из-за MDN:

В отличие от нативных событий, которые генерируются DOM и вызывают обработчики событий асинхронно через цикл событий, dispatchEvent вызывает обработчики событий синхронно. Все применимые обработчики событий будут выполнены и вернутся, прежде чем код продолжит выполнение после вызова dispatchEvent.