0

i wrote a debounce directive for my search field to prevent too much backend calls/processing. It's working as expected. However Im not able to write a spec for it. I hope some of you guys can help me out :) If you want to try it I created a plunker here. At the moment I presume, that the directive is not initialized as expected.

My directive

import { NgModel } from '@angular/forms';
import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';

@Directive({selector: '[debounce]'})
export class DebounceDirective implements OnInit {
  @Input() delay: number = 300;

  constructor(private elementRef: ElementRef, private model: NgModel) {
  }

  public ngOnInit(): void {
    const eventStream = fromEvent(this.elementRef.nativeElement, 'keyup').pipe(
      map(() => {
        return this.model.value;
      }),
      debounceTime(this.delay));

    this.model.viewToModelUpdate = () => {
    };

    eventStream.subscribe(input => {
      this.model.viewModel = input;
      this.model.update.emit(input);
    });
  }
}

My specs

import { DebounceDirective } from '@modules/shared/directives/debounce.directive';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { Component, DebugElement, ViewChild } from '@angular/core';
import { By } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

@Component({
  template: '<input type="text" name="test" debounce [delay]="500" [(ngModel)]="value" (ngModelChange)="test()">'
})
class DebounceDirectiveTestingComponent {
  @ViewChild(DebounceDirective) directive: DebounceDirective;
  public value: string = '';

  public test() {
  }
}

describe('Directive: Debounce', () => {
  let component: DebounceDirectiveTestingComponent;
  let fixture: ComponentFixture<DebounceDirectiveTestingComponent>;
  let inputEl: DebugElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [FormsModule],
      declarations: [DebounceDirective, DebounceDirectiveTestingComponent]
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(DebounceDirectiveTestingComponent);
    component = fixture.componentInstance;
    inputEl = fixture.debugElement.query(By.css('input'));
    fixture.detectChanges();
  });

  it('should create component', () => {
    expect(component).toBeTruthy();
  });

  it('should emit values after 500 msec debounce', fakeAsync(() => {
    const directiveEl = fixture.debugElement.query(By.directive(DebounceDirective));
    spyOn(component, 'test').and.stub();
    spyOn(component.directive, 'ngOnInit').and.callThrough();

    fixture.detectChanges();

    expect(directiveEl).toBeDefined();
    expect(component.directive.ngOnInit).toHaveBeenCalled();
    expect(component.directive.delay).toBe(500);

    inputEl.nativeElement.value = 'test';
    inputEl.nativeElement.dispatchEvent(new Event('keyup'));

    fixture.detectChanges();

    expect(component.value).toBe('');

    tick(500);
    fixture.detectChanges();

    expect(component.test).toHaveBeenCalled();
    expect(component.value).toBe('test');
  }));
});
trollr
  • 1,095
  • 12
  • 27
  • 1
    On thing with the onInit spy is, that onInit gets triggered with the first detectChanges call. So in your test you trigger the onInit before you actually spy on it. If you remove the fixture.detectChanges inside your beforeEach and instead add it to your first test case and leave the other as it is, your onInit gets called. There is still a problem with the value though. I'll see if I can find a solution to that too. – Erbsenkoenig Mar 26 '19 at 15:08
  • 1
    Not an answer, but interesting anyway - commenting out the first call to `fixture.detectChanges()` as @Erbsenkoenig pointed out, along with also changing the event type to be 'input' instead of 'keyup' in both the .spec and the directive has both tests passing - see this fork of your [StackBlitz](https://stackblitz.com/edit/angular-6-jasmine-xtpqna?file=src%2Fdebounce.directive.spec.ts). However, from your code I see you want to debounce each keypress, so I don't offer this as a solution. Looks like `this.model.value` is not being set properly with the `keyup` event ... – dmcgrandle Mar 27 '19 at 05:14
  • I also put a tap in before the map in your directive to show that whether you use an 'input' event or a 'keyup' event, the event sent into the observable pipe has the correct value in `event.target.value`. – dmcgrandle Mar 27 '19 at 05:22
  • Hey guys, thanks for ur help! I'll switch to the input event cuz there's no difference in my use case. Now its working fine. Someone of you guys should write a clear answer and i'll give you the rep. The Keyup event seems to be the major issue because its firing before the value is emitted (https://stackoverflow.com/questions/38502560/whats-the-difference-between-keyup-keydown-keypress-and-input-events) – trollr Mar 27 '19 at 08:07

1 Answers1

1

As suggested by @trollr, I am posting the solution that worked as an answer: changing the type of event from a 'keyup' event to an 'input' event.

In the directive:

const eventStream = fromEvent(this.elementRef.nativeElement, 'input').pipe(

And in the test:

inputEl.nativeElement.dispatchEvent(new Event('input'));

These changes, along with commenting out the first call to fixture.detectChanges() have resolved the issue.

Here is the StackBlitz showing all the tests passing.

halfer
  • 19,824
  • 17
  • 99
  • 186
dmcgrandle
  • 5,934
  • 1
  • 19
  • 38