Thursday, April 19, 2018

Testing Angular 5 Promises and Tick

I have a function that calls two other functions that return promises. Each promise has a callback (.then) chained to it, which will also return another promise (because that's how promises work). Once both promises are resolved and their callbacks are complete, I take one final action. It looks like this:
   1:  const promises: Promise<void>[] = [];
   2:  promises.push(this.service.doTheFirstThingThatReturnsAPromise().then(result => {
   3:    this.firstProp = true;
   4:  }));
   5:
   6:  promises.push(this.service.doTheSecondThingThatReturnsAPromise().then(result => {
   7:    this.secondProp = true;
   8:  }));
   9:
  10:  Promise.all(promises).then(() => {
  11:    this.thirdProp = true;
  12:  });

My problem arose when I tried to test this. It seemed like everything should have been working fine with this test:
   1:  beforeEach(() =>{
   2:    component.doTheThingThatDoesTheOtherThings();
   3:  });
   4:
   5:  it('should do some stuff', fakeAsync(()=>{
   6:    tick();
   7:    expect(component.firstProp).toBe(true);
   8:    expect(component.secondProp).toBe(true);
   9:    expect(component.thirdProp).toBe(true);
  10:  }));

I've done almost exactly this before (in this same project no less!) and it worked fine. The only difference was that in this case I decided to move my target function invocation inside my beforeEach and that made all the difference. With the function invocation happening inside the beforeEach, my entire test was completing before the tick was ever process (which I still don't fully understand, which is part of the reason I'm writing this up). All I had to do to get my tests to pass was move the target function invocation into my spec instead of the beforeEach. So my final (working) result looks like this:
   1:  it('should do some stuff', fakeAsync(()=>{
   2:    component.doTheThingThatDoesTheOtherThings();
   3:    tick();
   4:    expect(component.firstProp).toBe(true);
   5:    expect(component.secondProp).toBe(true);
   6:    expect(component.thirdProp).toBe(true);
   7:  }));

Fortunately, that only took me about 30 minutes to figure out. Hopefully next time I encounter something similar I check back here first and save myself that 30 minutes.

Tuesday, April 17, 2018

Angular: "Can't bind to 'ngClass' since it isn't a known property of 'div'"

I recently came across a very annoying little error in my new Angular component library (available on npm and Github). Everything worked fine while I was developing locally, but when we ran ng build --prod we got a rather confusing error message:

ERROR in : Can't bind to 'ngClass' since it isn't a known property of 'div'. ("<div [ERROR ->][ngClass]="config.containerClasses" class="ea-multi-select-dropdown-container"> <label eaMultiSelec")

It took me a couple of hours to find the problem, and Google wasn't really helpful. See, usually the problem is that you haven't imported one of the necessary modules (BrowserModule from @angular/platform-browser if your component is in the root module, or CommonModule from @angular/common if your component is part of a separate module). I found answers like this and this and this all over the place, but every time I went back and looked at my code everything looked right to me.

I finally decided to start the whole project from scratch and run ng build --prod every time I added or changed something. Fortunately, this was pretty early on in the process so I didn't have much to redo, but it was still a pain to do it. I got the major pieces added back in and everything still worked fine.

Then I realized that in my focus on starting over I had neglected to recreate the fakes and mocks that I like to include in my components. Let me back up and explain.

When I create new components I find it helpful to include a "Fake" version of the component for unit testing purposes. This way when a consume of the component writes their own tests they don't have to worry about the behavior of my component. Instead of declaring the actual component (e.g. EaMultiSelectDropdownComponent) they can reference the fake version of the component (e.g. FakeEaMultiSelectDropdownComponent). The fake has all of the same properties and functions, but does none of the actual work. In this particular case I just extended the real component and overrode the functions in the fake component. As is often the case, my haste lead to my problem. I copy/pasted the original component and then made my changes to make it a fake, but I never changed the templateUrl of the fake component.

It turned out that everything really was done properly in my actual component, but the fake module into which I registered my fake component wasn't importing CommonModule so everything blew up when the AOT build ran. I was glad it turned out to be such an easy fix, and I doubt most people will have the same issue (since creating fakes for your components doesn't seem to be a popular approach), but since this blog is dedicated to documenting Answers I Couldn't Find Anywhere Else it was worth writing up.