Angular testing - ngBootstraps typeahead
Asked Answered
I

2

10

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.

Ingraham answered 21/10, 2018 at 11:20 Comment(3)
Error is coming from text$.pipe( put a debug point or a console.log and check what is the value come thereManutius
Try to add spyOn(component, 'search').and.returnValue(of('some string')); Cavefish
@Cavefish I already tried that but it throws me search method does not exists on that component..Ingraham
P
6

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();
}));
Prime answered 25/10, 2018 at 21:1 Comment(2)
Thanks for the update - I updated my response based on your feedback.Prime
Thanks for the answer and congrats to the bounty.^^Ingraham
R
1

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.

Raeannraeburn answered 26/10, 2018 at 20:17 Comment(1)
I am now confused.^^ Is this a recomendation on design-patterns, because such a function should better fit in a service instead of a component? Or is this a solution approach on my unit testing failure? Interested for details...:)Ingraham

© 2022 - 2024 — McMap. All rights reserved.