74

I have a close function in my component that contains a setTimeout() in order to give time for the animation to complete.

public close() {
    this.animate = "inactive"
    setTimeout(() => {
       this.show = false
    }, 250)
}

this.show is bound to an ngIf.

this.animate is bound to an animation.

I have a test that needs to test this function

it("tests the exit button click", () => {
  comp.close()
  fixture.detectChanges()
  //verifies the element is no longer in the DOM
  const popUpWindow = fixture.debugElement.query(By.css("#popup-window"))
  expect(popUpWindow).toEqual(null)
})

How do you properly test this function when there is a setTimeout()?

I was using jasmine.clock().tick(251) but the window would never disappear. any thoughts on this as well?

ed-tester
  • 1,596
  • 4
  • 17
  • 24
  • Have you tried using `done`? – jonrsharpe Jan 20 '17 at 22:06
  • @jonrsharpe How exactly – ed-tester Jan 20 '17 at 22:06
  • Then I'd recommend you start your research there; it's for testing asynchronous processes. – jonrsharpe Jan 20 '17 at 22:07
  • Not what you originally asked, but instead of using a timeout, you can use the `done` event that Angular fires. In your template, you can use `` -- the `.done` event code will run immediately after your animation completes. – Coderer Oct 13 '20 at 10:56
  • **See also** [test setTimeout with Jasmine (but witout Angular)?](https://stackoverflow.com/questions/10955201/how-to-test-a-function-which-has-a-settimeout-with-jasmine) – Top-Master Mar 15 '22 at 17:45

5 Answers5

136

You could do one of two things:

1: Actually wait in the test 250+1 ms in a setTimeout(), then check if the element actually disappeared.

2: use fakeAsync() and tick() to simulate time in the test - a tick() will resolve the setTimeout in the original close(), and the check could happen right after in a fixture.whenStable().then(...).

For example:

it("tests the exit button click", fakeAsync(() => {
  comp.close()
  tick(500)
  fixture.detectChanges()

  fixture.whenStable().then(() => {
    const popUpWindow = fixture.debugElement.query(By.css("#popup-window"))
    expect(popUpWindow).toBe(null)
  })
}))

I suggest using the 2nd one, as it is much more faster than actually waiting for the original method. If you still use the 1st, try lowering the timeout time before the test to make the it run faster.

SEVICES

For services you do not need to call detectChanges after tick and do not need to wrap the expect statements within whenStable. you can do your logic right after tick.

  it('should reresh token after interval', fakeAsync(() => {
    // given
    const service: any = TestBed.get(CognitoService);
    const spy = spyOn(service, 'refreshToken').and.callThrough();
    ....
    // when
    service.scheduleTokenRefresh();
    tick(TOKEN_REFRESH_INTERVAL);
    // then
    expect(spy).toHaveBeenCalled();
  }));
Bruno João
  • 5,105
  • 2
  • 21
  • 26
Mezo Istvan
  • 2,664
  • 1
  • 14
  • 17
  • 1
    Thank you. The second one worked, but was a little more complex then simply using tick(). I had to wrap the `expects` within a `fixture.whenStable().then( ... //expects )`. If you allow me to update your answer with this additional info I will accept it as the correct answer. – ed-tester Jan 23 '17 at 16:03
  • 4
    To add to that, you may need an additional `fixture.detectChanges()` *before* the `tick`. Then you may not need `fixture.whenStable()` at all (afaik fakeAsync was designed precisely to avoid such async calls). – dan Nov 02 '17 at 21:19
  • 6
    incase it helps anyone else, the function that calls the setTimeout needs to be called within the fakeAsync method (can't be called in the beforeEach). Seems obvious but it got me! – Sabrina Leggett Jul 02 '18 at 14:54
  • try jasmine.clock().tick(10); – Jack Luo Oct 19 '18 at 16:29
  • ATTENTION: adding ````fixture.whenStable()```` is not only unneccessary (as mentioned by @dinvlad) but may also lead to tests passing although they are not correct. I tried this approach and my tests passed no matter what I expected. ````fixture.whenStable()```` should be deleted from the code block. More information: https://stackoverflow.com/questions/42971537/what-is-the-difference-between-fakeasync-and-async-in-angular-testing – MikhailRatner Aug 11 '22 at 10:28
30

In my component the method is:

hideToast() {
    setTimeout( () => {
      this.showToast = false;
    }, 5000);
  }

Test for that (explanation in comments):

it('should hide toast', () => {
  component.showToast = true; // This value should change after timeout
  jasmine.clock().install();  // First install the clock
  component.hideToast();      // Call the component method that turns the showToast value as false
  jasmine.clock().tick(5000); // waits till 5000 milliseconds
  expect(component.showToast).toBeFalsy();  // Then executes this
  jasmine.clock().uninstall(); // uninstall clock when done
});

Hope this helps!!

Kailas
  • 7,350
  • 3
  • 47
  • 63
6

Just tried this in my project and works:

jasmine.clock().tick(10);
buzatto
  • 9,704
  • 5
  • 24
  • 33
Jack Luo
  • 333
  • 3
  • 5
5

I have a method that is eerily similar to the OP's setup so thought I would add my test which I think is a lot simpler.

** Method **

public close() {
    this.animate = "inactive"
    setTimeout(() => {
       this.show = false
    }, 250)
}

** Jasmine Test **

it('should set show to "false" after 250ms when close is fired', (done) => {
      component.close();
      setTimeout(() => {
        expect(component.close).toBeFalse();
        done();
      }, 251);
    });

Note the use of 'done' to let jasmine know the test is finished and also adding 1 ms to the setTimeout to fire after my method's setTimeout.

jpizzo
  • 143
  • 2
  • 7
2

this solution mentioned by @Kailas works for me:

hideToast() {
    setTimeout( () => {
      this.showToast = false;
    }, 5000);
  }

it('should hide toast', () => {
  component.showToast = true; // This value should change after timeout
  jasmine.clock().install();  // First install the clock
  component.hideToast();      // Call the component method that turns the showToast value as false
  jasmine.clock().tick(5000); // waits till 5000 milliseconds
  expect(component.showToast).toBeFalsy();  // Then executes this
  jasmine.clock().uninstall(); // uninstall clock when done
});

but sometimes you will receive this error:

Error: Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?

to resolve this issue you need to uninstall the clock first:

it('should hide toast', () => {
  jasmine.clock().uninstall()
  component.showToast = true; 
  jasmine.clock().install();  
  component.hideToast();      
  jasmine.clock().tick(5000); 
  expect(component.showToast).toBeFalsy();  
  jasmine.clock().uninstall(); 
});
Hamid Hoseini
  • 1,560
  • 17
  • 21