5

I'm trying to test an Angular Component that basically receives an Observable and changes its template based on the values emitted from that Observable. Here's a simplified version:

@Component({
    selector: 'async-text',
    template: `
        <span>{{ text | async }}</span>
    `,
})
export class AsyncTextComponent {    
    @Input() text: Observable<string>;
}

I'd like to test it out, and currently this is what I have, using rxjs-marbles (though it's not a must).

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AsyncTextComponent } from './async-text.component';

describe('AsyncTextComponent', () => {
  let component: BannerComponent;
  let fixture: AsyncTextComponent<AsyncTextComponent>;

  it('...',
    marbles(m => {
      fixture = TestBed.createComponent(AsyncTextComponent);
      component = fixture.componentInstance;
      component.text = m.cold('-a-b-c|', {
        a: 'first',
        b: 'second',
        c: 'third',
      });

      fixture.detectChanges();
      expect(component.nativeElement.innerHTML).toContain('first');

      fixture.detectChanges();
      expect(component.nativeElement.innerHTML).toContain('second');

      fixture.detectChanges();
      expect(component.nativeElement.innerHTML).toContain('third');
    })
  );
});

Obviously this doesn't work. My issue is that I didn't find a way to advance the TestScheduler by a given amount of 'frames' between each expect.

How can I manually skip frames? Or alternatively, is there a better way to test the above component/scenario (Angular component that receives an Observable and I want to test it's behaviour given the Observable's emittions).

I did see .flush(), but as documented, it runs all of the Observable's emits, so I'd get to the final status, and can't test out different transitions between states.

Thanks

bengr
  • 316
  • 3
  • 8
  • You should not pass an observable as input – maxime1992 May 24 '18 at 14:38
  • Take away the complexity and use async pipe in the parent component – maxime1992 May 24 '18 at 14:38
  • The above is a simplified component. In really it does more than just show the incoming text, but instead receives an `Observable`, and based on the value of `Flag` emitted, displays different templates. For example, `Flag.Loading` would show a loading indicator, and when `Flag.Complete` is emitted another template is shown. – bengr May 24 '18 at 14:43
  • Ok. Doesn't change the fact that you should use async on the parent when calling child component so it only get the values. – maxime1992 May 24 '18 at 14:44
  • I agree in principal, but in reality this increases the boilerplate around this component, since you have to translate an `Observable` throwing an error or completing to one of these `Flag`s, and only then pass it. – bengr May 24 '18 at 14:48
  • Often, if it's hard to test... There's a code smell. – maxime1992 May 24 '18 at 14:49
  • Testing a dumb component and it's inputs: trivial. Testing an observable: simple. Mix an observable pass as an input and that won't help. Complexity does not disappear, you're just shifting it somewhere else. – maxime1992 May 24 '18 at 14:51
  • There's the `.flush()` method on the `TestScheduler` of `rxjs`, but it fires all the events, making the observable finish it's run. I want to stop after each emission basically. I saw that it was possible to do with [rxjs 4](https://chrisnoring.gitbooks.io/rxjs-5-ultimate/content/testing.html) (see beginning there), but no longer is in 5 & 6. – bengr May 24 '18 at 14:52
  • @maxime1992 what do you mean? – bengr May 24 '18 at 14:53

1 Answers1

2

You should not have to use any library to test it. Even more, you can test it outside of Angular's context.

Anyway, here is the explanation.

To test this, I would recommend using variables. But if you want to stay with your method, you should go with that.

it('should display first', done => {
  // Mock your component
  component.text = Observable.of('first');
  // Detect template changes
  fixture.detectChanges();
  // trigger a change detection, just in case (try without, you never know)
  setTimeout(() => {
    // Get the element that is displaying (tip: it's not your whole component)
    const el = fixture.nativeElement.querySelector('span');
    // Test the innet text, not the HTML
    // Test with includes, in case you have spaces (but feel free to test without includes)
    expect(el.innerText.includes('first')).toEqual(true);
    // End your test
    done();
  });
});
  • You should rather use fakeAsync and tick. Nothing guarantees you that the observable is going to emit during the setTimeout. Also, if the observable emits after 10s, are you going to put a 10s setTimeout? Your tests will take ages. With fakeAsync you get control over the time and it'll be instantaneous – maxime1992 May 24 '18 at 14:48
  • This doesn't deal with testing the state changes. See in my OP that I want to test that when `'first'` is emitted that the template shows "first", when `'second'` is emitted that the template shows "second" and so forth. – bengr May 24 '18 at 14:49
  • 1
    @maxime1992 the Observable is mocked. You already have control on the emitting. as for the OP, i wrote the logic, I won't write the whole test, I'm pretty sure you can figure it on your own with that ! –  May 24 '18 at 14:51