83

This is hardly first encounter I've had with "1 timer(s) still in the queue", but usually I find some way to use tick() or detectChanges(), etc., to get out of it.

The test below was working fine until I tried to test for a condition that I know should throw an exception:

  it('should be able to change case', fakeAsync(() => {
    expect(component).toBeTruthy();

    fixture.whenStable().then(fakeAsync(() => {
      component.case = 'lower';
      fixture.autoDetectChanges();
      tick(500);
      const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
      typeInElement('abcDEF', input);
      fixture.autoDetectChanges();
      tick(500);
      expect(component.text).toEqual('abcdef');

      component.case = 'upper';
      fixture.autoDetectChanges();
      tick(500);
      typeInElement('abcDEF', input);
      fixture.autoDetectChanges();
      tick(500);
      expect(component.text).toEqual('ABCDEF');

      // Everything above works fine. Here's where the trouble begins
      expect(() => {
        component.case = 'foo';
        fixture.autoDetectChanges();
        tick(500);
      }).toThrowError(/Invalid case attribute/);
    }));
  }));

What I'm testing is an Angular component that's a wrapper around a Material input field. The component has many optional attributes, most of them just pass-through attributes for common input field features, but a few custom attributes too, like the one I'm testing above for upper-/lowercase conversion.

The acceptable values for the case attribute are upper, lower, and mixed (with empty string, null, or undefined treated as mixed). The component should throw an exception for anything else. Apparently it does, and the test succeeds, but along with the success I get:

ERROR: 'Unhandled Promise rejection:', '1 timer(s) still in the queue.', '; Zone:', 'ProxyZone', '; Task:', 'Promise.then', '; Value:', Error: 1 timer(s) still in the queue.
Error: 1 timer(s) still in the queue.
   ...

Can anyone tell me what I might be doing wrong, or a good way to flush out lingering timers?

Disclaimer: A big problem when I go looking for help with Karma unit tests is that, even when I explicitly search for "karma", I mostly find answers for Pr0tractor, Pr0tractor, and more Pr0tractor. This isn't Pr0tractor! (Deliberately misspelled with a zero so it doesn't get search matches.)

UPDATE: I can work around my problem like this:

      expect(() => {
        component.inputComp.case = 'foo';
      }).toThrowError(/Invalid camp-input case attribute/);

This isn't as good of a test as assigning the (bad) value via an HTML attribute in the test component's template, because I'm just forcing the value directly into the component's setter for the attribute itself, but it'll do until I have a better solution.

kshetline
  • 12,547
  • 4
  • 37
  • 73
  • OK, aside from the test issue, why don't you define the property either as an enum or as a union of allowable strings, so this can get caught at compile time? – theMayer Oct 16 '19 at 17:45
  • Actually, it is defined that way (`type InputCase = 'lower' | 'mixed' | 'upper';`), but attributes assigned via HTML don't reliably get type checked. – kshetline Oct 16 '19 at 17:49
  • And by the way, what was your problem with my disclaimer? – kshetline Oct 16 '19 at 17:51
  • Also, by using a setter that checks validity, the value can also be case-insensitive. – kshetline Oct 16 '19 at 17:55
  • I would avoid setting values in html. If you can’t avoid it, then I would not raise an exception- instead perhaps log to the console and select a sensible default. My problem with the disclaimer was that it had nothing to do with the question. – theMayer Oct 16 '19 at 19:05
  • 1
    It's a pretty important feature of Angular (and not a mis-feature) that you can create custom components and then have users use those components as they wish *in HTML*, like ``. As for raising exceptions vs. quietly failing with a console warning, there are pluses and minuses to both approaches, and in this case it's not my call anyway. – kshetline Oct 16 '19 at 19:23

5 Answers5

122

I have faced with the similar problem. The solution was flush function usage.

import { fakeAsync, flush } from '@angular/core/testing';

it('test something', fakeAsync(() => {

  // ...

  flush();
}));
SternK
  • 11,649
  • 22
  • 32
  • 46
  • FYI: whilst working with button clicks and requiring their side effects to be accounted for I stumbled upon a legitimate use case for fakeAsync! The requirement for using tick() swung it.. in "async" block equivalent would have been setTimeout(()=>{},0) but alongside using fixture.whenStable() more of a code bloat.. – Kieran Ryan Mar 11 '21 at 17:10
93

I ran into the same issue recently - to resolve I called discardPeriodicTasks() -from @angular/core/testing at the end of my it function and my tests passed after that.

In this scenario you may want to insert it before your final expect

 it('should be able to change case', fakeAsync(() => {
    expect(component).toBeTruthy();

    fixture.whenStable().then(fakeAsync(() => {
      component.case = 'lower';
      fixture.autoDetectChanges();
      tick(500);
      const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
      typeInElement('abcDEF', input);
      fixture.autoDetectChanges();
      tick(500);
      expect(component.text).toEqual('abcdef');

      component.case = 'upper';
      fixture.autoDetectChanges();
      tick(500);
      typeInElement('abcDEF', input);
      fixture.autoDetectChanges();
      tick(500);
      expect(component.text).toEqual('ABCDEF');

      discardPeriodicTasks() <-------------------- try here

      // Everything above works fine. Here's where the trouble begins
      expect(() => {
        component.case = 'foo';
        fixture.autoDetectChanges();
        tick(500);
      }).toThrowError(/Invalid case attribute/);
      
    }));

tick acts to move the time forward in your fakeAsync context.

flush acts to simulate the completion of time in that context by draining the macrotask queue till it is empty.

discardPeriodicTasks "throws out" any remaining periodic tasks.

They each serve different purposes and will have different use cases.

brooklynDadCore
  • 1,309
  • 8
  • 13
  • 1
    I've moved on from the project where I had this problem a while ago, but I should bookmark this answer to give it a try the next time I get back to doing any more Angular unit tests. – kshetline Jan 24 '20 at 18:51
  • 2
    It really saved me a lot of headache this morning, so I wanted to share. Best – brooklynDadCore Jan 24 '20 at 19:07
  • flush() worked for me as last instruction in unit test.. unfortunately discardPeriodicTasks() made no effect :-) – Kieran Ryan Mar 11 '21 at 17:05
  • I'm just getting back to some Angular unit testing after a long break from it. Same error again for a new test. Unfortunately, discardPeriodicTasks() didn't help. – kshetline Aug 04 '22 at 21:20
  • Ah, hah! This time around flush() did the trick. That didn't used to work reliably for me, but perhaps the testing methods have been improved since the last time I struggled with this. – kshetline Aug 04 '22 at 21:29
  • ````flush()```` *DID NOT* work for me but ````discardPeriodicTasks()```` did, thanks! – MikhailRatner Aug 15 '22 at 14:24
4

You can use discardPeriodicTasks() method in your last line of test case to get rid of remaining 'timer still in the queue' issue

3

Use flush() at the end of your test which is failing. I was also facing this issue and used this solve it.

It will be imported from @angular/core/testing.

Gerhard
  • 6,850
  • 8
  • 51
  • 81
TK Singh
  • 29
  • 1
0

In my case, I deleted the line somewhere in my code to be tested:

this.changeDetection.detectChanges();

Then the test worked like a charm.