-1

I have a form field (username) in a component that when updated calls a service to check for the value's availability in a database. Everything checks out fine but I can't seem to trigger the field's update function.

Here's the excerpt from the component code:

 ngAfterViewInit() {
   this.username.update
    .debounceTime(500) // wait 1/2 second before emitting last event
    .distinctUntilChanged() // only emit value if different from last value
    .subscribe((username: string) => {

      console.log('IN SUBSCRIBE'); // <==== This is not being reached in the tests

      let data = { type: 'username', val: username };
      this._loginSrvc.chkUser(data).subscribe((response: IChkUserResponse)=>{
        if(response.isAvailable === false)
          this.username.control.setErrors({'taken': {value: this.username.value}});
      });
    });
 }

Here's the field in the component's template:

<mat-form-field fxFlex>
  <input matInput placeholder="Username" aria-label="Username" forbiddenCharacters=';"\\\/\[\]\{\}\(\)' required [(ngModel)]="model.username" #username="ngModel" id="username" name="username">
  <mat-error *ngIf="newUserFrm.hasError('required','username')">Username is required</mat-error>
  <mat-error *ngIf="newUserFrm.hasError('forbiddenCharacters','username')">{{forbiddenChars.error}}</mat-error>
  <mat-error *ngIf="newUserFrm.hasError('taken','username')">The username <em>{{model.username}}</em> is taken</mat-error>
</mat-form-field>

Here's my test:

it('should check for username availability',fakeAsync(()=>{
  spyOn(service,'chkUser');

  let input = fixture.debugElement.query(By.css('#username')).nativeElement;
  expect(input.value).toBe('');
  expect(component.username.value).toBe(null);

  input.value = user.username;
  input.dispatchEvent(new Event('input'));

  tick(500);

  expect(service.chkUser).toHaveBeenCalled();
}));

That last expect(service.chkUser).toHaveBeenCalled() is what is causing my test to fail. I've run this component in an application and it works as expected I'm just trying to get the test to pass. I've tried so many combinations of setting the input value and dispatching the event, waiting on the fixture to detectChanges or using whenStable(), throwing in a promise, using fakeAsync not using fakeAsync. Nothing seems to work.

m.e.conroy
  • 3,508
  • 25
  • 27
  • Are you sure that subscription is created, that `ngAfterViewInit` gets called? – jonrsharpe Feb 22 '18 at 22:22
  • Yes, its working in the application, i've tested it manually. – m.e.conroy Feb 22 '18 at 22:24
  • I mean *in the test*. Have you put any logs in to find out where it's getting? Run a debugger? – jonrsharpe Feb 22 '18 at 22:25
  • Would this not create the component as if it were instantiated in the application itself: `fixture = TestBed.createComponent(NewUserComponent);` which in my `beforeEach()`. How would I test to see if `ngAfterViewInit` was called or not in my test? – m.e.conroy Feb 22 '18 at 22:28
  • I'm not sure that fakeAsync is applicable here, the test uses real dom and thus really async. Try async() instead, and real delays for this reason. It could be 550 and not 500, as well. – Estus Flask Feb 22 '18 at 22:29
  • @estus I've tried that too, I get the same error either way. I've also set tried setting the delay to be 100ms after the actual and still nothing. – m.e.conroy Feb 22 '18 at 22:30
  • That creates the component, yes. You could test it, as I hinted above, by putting a log in that method and seeing if it gets hit. – jonrsharpe Feb 22 '18 at 22:31
  • @jonrsharpe I added a `console.log('AFTER_VIEW_INIT')` to the actual `ngAfterViewInit` and it does get called when I run the test. – m.e.conroy Feb 22 '18 at 22:34
  • Can you provide an example of how you did this, too? Did you try detectChanges() before/after tick() in fakeAsync? – Estus Flask Feb 22 '18 at 22:35
  • Alright. And did you put any logs in the observable stream with e.g. `.do`? In the callback? Where does it get to? – jonrsharpe Feb 22 '18 at 22:36
  • @estus yup did that too, tried adding `fixture.detectChanges()` after the input event, after the tick, it is called in the `beforeEach()`. I've even tried using the `fixture.whenStable()` and the adding the test the callback function of the promise returned by that method. Still nothing. – m.e.conroy Feb 22 '18 at 22:37
  • @jonrsharpe Yes I added a log call to the subscribe function of `this.username.update` it does not reach that output, so I guess the event isn't dispatching correctly. – m.e.conroy Feb 22 '18 at 22:39
  • I should add that the service that is supposed to be called is passing all tests and working as it should, separately from this component. – m.e.conroy Feb 22 '18 at 22:45
  • @jonrsharpe I edited the post to show where in the `ngAfterViewInit` function the test is not reaching. – m.e.conroy Feb 22 '18 at 22:47
  • do you think you could create a stackblitz with the minimal code to try to recreate this? – Boris Lobanov Feb 22 '18 at 22:50
  • also, try to spy on username.update method, is that being called? – Boris Lobanov Feb 22 '18 at 22:51
  • @BorisLobanov just spied `username.update` its not being called, but I already knew that because the console output in its subscribe function isn't being reached. – m.e.conroy Feb 22 '18 at 22:55
  • this `input.dispatchEvent(new Event('input'));` doesn't seem to be triggering the update method. I've even tried `Event('keyup')` – m.e.conroy Feb 22 '18 at 22:56
  • @BorisLobanov I'll see what i can do about getting something up on StackBlitz, but it will have to wait for the time being – m.e.conroy Feb 22 '18 at 22:59
  • I guess, the sooner you do this the more chances you have of getting help :) – Boris Lobanov Feb 22 '18 at 23:02
  • can you at least post the code for the username.update method? and the username? – Boris Lobanov Feb 22 '18 at 23:07
  • @BorisLobanov there is no code for `username.update` and `username`, `this.username` is a reference to the `ngModel` of the form input control. – m.e.conroy Feb 22 '18 at 23:09
  • @BorisLobanov `username.update` is an `EventEmitter` – m.e.conroy Feb 22 '18 at 23:12
  • what's `model`? – Boris Lobanov Feb 22 '18 at 23:15
  • @BorisLobanov https://angular.io/api/forms/NgModel – m.e.conroy Feb 22 '18 at 23:16
  • **Update** I finally got the subscribe function to fire by doing the following `component.username.update.emit(null);` but now its saying the service being called is undefined. – m.e.conroy Feb 22 '18 at 23:25
  • can you show your code that configures your TestBed e.g. `TestBed.configureTestingModule({...})`, suspect your are not injecting dependencies – than Feb 23 '18 at 03:53
  • Why the down vote? This is a legitimate question and problem I was having and supplied more than enough information. – m.e.conroy Feb 23 '18 at 13:55

1 Answers1

1

So I figured out my problem after too many hours of frustration. It was thanks to those that were commenting that got me thinking in the right direction so thank you @jonrsharpe and @BorisLobanov.

So apparently the update EventEmitter wasn't firing and input.dispatchEvent(new Event('input')); wasn't making it so. That was the first clue. I had to make sure that the event fires in order for the call to the service to be reached.

In order to do this I was already grabbing the `componentInstance' of the component from the TestBed.

 beforeEach(() => {
   fixture = TestBed.createComponent(NewUserComponent);
   component = fixture.componentInstance;
   fixture.detectChanges();
 }

In order to get the event to fire, I needed to access the field's underlying ngModel grab its update EventEmitter and emit something.

So in my test I've added this line:

 component.username.update.emit(null);

This got my field to run the debounced subscribe function attached to the event.

This however did not fix the entire problem, running the test gave me a cannot execute subscribe on undefined. The subscribe causing the error was the one attached to the service call this._loginSrvc.chkUser(data).subscribe((...)=>{...}).

After hours of fiddling with Observables and recreating my mocked services I finally stumbled upon a stackoverflow answer in another question that stated simple to remember to add .and.callThrough() when spying on a service's method. OMG! How could I have forgotten that (slaps hand against forehead). The subscribe wasn't functioning because the spyOn(service,'chkUser') was stopping the execution of the actual method, so there was no Observable to subscribe too. In every example I saw that was attempting to do something similar never did they add the .and.callThrough() to their spy so it never occurred to me even the countless times I've used this before.

Anyway here's the working test:

it('should check for username availability',fakeAsync(()=>{
  spyOn(service,'chkUser').and.callThrough();

  let input = fixture.debugElement.query(By.css('#username')).nativeElement;
  expect(input.value).toBe('');
  expect(component.username.value).toBe(null);

  input.value = user.username;

  component.username.update.emit(null);

  tick(500);

  expect(service.chkUser).toHaveBeenCalled();
}));
m.e.conroy
  • 3,508
  • 25
  • 27