Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular testing - ngBootstraps typeahead

I am currently using a autocomplete mechanism (typeahead) of ngBootstrap. Now I want to unit test if a method is called on every sequence of an input event. The error on my test case is currently: Cannot read property 'pipe' of undefined

Html:

<input id="locationEdit" type="text" class="form-control"
         [(ngModel)]="user.location" name="location [ngbTypeahead]="search"/>

Component:

public ngOnInit() {
    this.search = (text$: Observable<string>) =>
      text$.pipe(
        tap(() => {
          this.isSearching = true;
          this.searchFailed = false;
        }),
        debounceTime(750),
        distinctUntilChanged(),
        switchMap(term =>
          this.cityService.getLocation(term).pipe(
            tap((response) => {
              this.searchFailed = response.length === 0;
              this.isSearching = false;
            })))
      );
  }

spec.ts

  it('should call spy on city search', fakeAsync(() => {
    component.user = <User>{uid: 'test', username: 'mleko', location: null, description: null};
    const spy = (<jasmine.Spy>cityStub.getLocation).and.returnValue(of['München Bayern']);

    fixture.detectChanges();
    const compiled: DebugElement = fixture.debugElement.query(By.css('#locationEdit'));
    compiled.nativeElement.value = 'München';
    compiled.nativeElement.dispatchEvent(new Event('input'));

    tick(1000);
    fixture.detectChanges();

    expect(spy).toHaveBeenCalled();
  }));

Can someone help me to mock this.search properly?

Edit

By the awesome suggestion of @dmcgrandle I don't need to render the HTML and simulate an input-event, to check if the typeahead is working. I rather should make an Observable, which emits values and assigns it to the function. One approach is:

  it('should call spy on city search', fakeAsync(() => {
    const spy = (<jasmine.Spy>cityStub.getLocation).and.returnValue(of['München Bayern']);

    component.ngOnInit();
    const textMock = of(['M', 'Mün', 'München']).pipe(flatMap(index => index));

    component.search(textMock);

    tick();

    expect(spy).toHaveBeenCalled();
  }));

But the problem still is, component.search does not call the spy. Within the search function in the switchMap operator I added a console.log to see if value are emitted from the function. But that is not the case.

like image 362
MarcoLe Avatar asked Oct 21 '18 11:10

MarcoLe


2 Answers

I don't think you actually want to call any ngBootstrap code during your test - after all you want to unit test your code, not theirs. :)

Therefore I would suggest mocking the user actually typing by setting up a timed Observable of your own, and calling your function with it. Perhaps mock sending a character every 100ms. Something like this:

it('should call spy on city search', fakeAsync(() => {
    component.user = <User>{uid: 'test', username: 'mleko', location: null, description: null};
    // Change next line depending on implementation of cityStub ...
    const spy = spyOn(cityStub, 'getLocation').and.returnValue(of('München Bayern'));

    fixture.detectChanges();
    let inputTextArray = ['M', 'Mü', 'Mün', 'Münc', 'Münch', 'Münche', 'München'];
    let textMock$ : Observable<string> = interval(100).pipe(take(7),map(index => inputTextArray[index]));
    component.search(textMock$);
    tick(1000);
    expect(spy).toHaveBeenCalled();
}));

Update:

I put together a stackblitz here to test this out: https://stackblitz.com/edit/stackoverflow-question-52914753 (open app folder along the left side and click on my.component.spec.ts to see the test file)

Once I got it in there, it was obvious what was wrong - the observable was not being subscribed to because the subscription seems to be done by ngBootstrap, so for testing we need to subscribe explicitly. Here is my new suggested spec (taken from the stackblitz):

it('should call spy on city search', fakeAsync(() => {
    const cityStub = TestBed.get(CityService);
    const spy = spyOn(cityStub, 'getLocation').and.returnValue(of('München Bayern'));

    fixture.detectChanges();
    let inputTextArray = ['M', 'Mü', 'Mün', 'Münc', 'Münch', 'Münche', 'München'];
    let textMock$ : Observable<string> = interval(100).pipe(take(7),map(index => inputTextArray[index]));
    component.search(textMock$).subscribe(result => {
         expect(result).toEqual('München Bayern');
    });
    tick(1000);
    expect(spy).toHaveBeenCalled();
}));
like image 131
dmcgrandle Avatar answered Sep 27 '22 17:09

dmcgrandle


Please try moving the observable inside of the service:

Component:

this.cityService.text$.pipe

Service:

export class CityService {
private _subject = null;
text$ = null;

constructor(private _httpClient: HttpClient) {
    this.init();
}

init() {
    this._subject = new BehaviorSubject<any>({});
    this.text$ = this._subject.asObservable();
}

I can expand on my answer if you need more details.

like image 32
mruanova Avatar answered Sep 27 '22 17:09

mruanova