Эмуляция событий в 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.