angular2: how to test a component which has an observable time interval
Asked Answered
S

2

7

I have a slide-show component that has an Input array of slide objects and shows each one as long as it's been defined in slide.time of itself. also there are two buttons that clicking them has to slide to the next item and reset the timer. in order to make this work, I'm using Observables like this:

/**
 * a "SUBJECT" for pausing(restarting) the slider's auto-slide on user's click on left and right arrows
 * @type {Subject}
 */
private pauser = new Subject();

/**
 * the main observable for timer (before adding the pause/reset option)
 * @type {Observable<T>}
 */
private source = Observable
    .interval(1000)
    .timeInterval()
    .map(function (x) { /*return x.value + ':' + x.interval;*/ return x })
    .share();

/**
 * the final timer, which can be paused
 * @type {Observable<R>}
 */
private pausableSource = this.pauser.switchMap(paused => paused ? Observable.never() : this.source);

/**
 * the subscription to the timer which is assigned at OnInit hook , and destroyed at OnDestroy
 */
private subscription;

ngOnInit(){
    this.subscription = this.pausableSource.subscribe(() => {
        //doing changes to the template and changing between slides
    });
    this.pauser.next(false);
}

ngOnDestroy() {
    this.subscription.unsubscribe();
}

and it's working properly. now to test this component, I'm giving it some data within a test-host component and want to check it's functionality like this:

it("should show the second (.slidingImg img) element after testHost.data[0].time seconds 
    have passed (which here, is 2 seconds)", () => {
    //testing
});

i tried many things that i found in docs or anywhere on the internet, but none of them work for me. the problem is that I can't mock the passage of time in a way that the observable timer would perform next actions, and it's like no time has passed whatsoever. two of the ways that haven't worked for me are these:

it("should show the second (.slidingImg img) element after testHost.data[0].time seconds 
    have passed (which here, is 2 seconds)", fakeAsync(() => {
    fixture.detectChanges();
    tick(2500);
    flushMicrotasks();
    fixture.detectChanges();
    let secondSlidingImg = fixture.debugElement.queryAll(By.css('.slidingImg'))[1].query(By.css('img'));
    expect(secondSlidingImg).toBeTruthy();
    //error: expected null to be truthy
}));

i got this from angular2 docs.

and:

beforeEach(() => {
    fixture = TestBed.createComponent(TestHostComponent);

    testHost = fixture.componentInstance;

    scheduler = new TestScheduler((a, b) => expect(a).toEqual(b));

    const originalTimer = Observable.interval;
    spyOn(Observable, 'interval').and.callFake(function(initialDelay, dueTime) {
        return originalTimer.call(this, initialDelay, dueTime, scheduler);
    });
    // or:
    // const originalTimer = Observable.timer;
    // spyOn(Observable, 'timer').and.callFake(function(initialDelay, dueTime) {
    //     return originalTimer.call(this, initialDelay, dueTime, scheduler);
    // });
    scheduler.maxFrames = 5000;

});
it("should show the second (.slidingImg img) element after testHost.data[0].time seconds 
    have passed (which here, is 2 seconds)", async(() => {
    scheduler.schedule(() => {
        fixture.detectChanges();
        let secondSlidingImg = fixture.debugElement.queryAll(By.css('.slidingImg'))[1].query(By.css('img'));
        expect(secondSlidingImg).toBeTruthy();
        //error: expected null to be truthy
    }, 2500, null);
    scheduler.flush();
}));

i got this approach from this question.

so I desperately need to know how exactly should I simulate time passage in my unit test so that the component's observable time interval would really trigger...

versions:

angular: "2.4.5"
"rxjs": "5.0.1"
"jasmine": "~2.4.1"
"karma": "^1.3.0"
"typescript": "~2.0.10"
"webpack": "2.2.1"    
Sell answered 28/4, 2017 at 7:10 Comment(2)
Victor Savkin wrote an excellent blog post about that, you should read it :)Tayib
did you mean a special part of the post? because I just read it a couple of times and I think it's only relative solution for me is the use of fakeAsync and tick(/*some second*/) which is the first solution that i mentioned and is not working for me...Sell
F
0

fakeAsync doesn't work for some case with RxJs. You need to manually move internal timer in RxJs. Something along those lines:

import {async} from 'rxjs/internal/scheduler/async';
...

describe('faking internal RxJs scheduler', () => {
  let currentTime: number;

  beforeEach(() => {
    currentTime = 0;
    spyOn(async, 'now').and.callFake(() => currentTime);
  });

  it('testing RxJs delayed execution after 1000ms', fakeAsync(() => {
    // Do your stuff
    fixture.detectChanges();
    currentTime = 1000;
    tick(1000);
    discardPeriodicTasks();

    expect(...);
  }));
});
Feliciafeliciano answered 19/3, 2019 at 9:43 Comment(0)
C
0

Best way I found for testing this is Marble Testing:

Tutorial: https://medium.com/@bencabanes/marble-testing-observable-introduction-1f5ad39231c

You can control emits by order and time, which seems to be exactly what you need.

Charades answered 1/2, 2022 at 22:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.