5

I'm upgrading our Angular2 app to use rc4 and I started getting an error on my unit tests:

Cannot use setInterval from within an async zone test

My widget makes a request for data from its ngOnInit method and puts up a loading indicator while that request is made. My mocked service returns some data after 1ms.

Here's a simplified version that exposes the problem

import { inject, async, TestComponentBuilder, ComponentFixture} from '@angular/core/testing';
import {Http, Headers, RequestOptions, Response, HTTP_PROVIDERS} from '@angular/http';
import {provide, Component} from '@angular/core';

import {Observable} from "rxjs/Rx";

class MyService {
    constructor(private _http: Http) {}
    getData() {
        return this._http.get('/some/rule').map(resp => resp.text());
    }
}

@Component({
    template: `<div>
      <div class="loader" *ngIf="_isLoading">Loading</div>
      <div class="data" *ngIf="_data">{{_data}}</div>
    </div>`
})
class FakeComponent {
    private _isLoading: boolean = false;
    private _data: string = '';

    constructor(private _service: MyService) {}

    ngOnInit() {
        this._isLoading = true;
        this._service.getData().subscribe(data => {
            this._isLoading = false;
            this._data = data;
        });
    }
}

describe('FakeComponent', () => {
    var service = new MyService(null);
    var _fixture:ComponentFixture<FakeComponent>;

    beforeEach(async(inject([TestComponentBuilder], (tcb:TestComponentBuilder) => {
        return tcb
            .overrideProviders(FakeComponent, [
                HTTP_PROVIDERS,
                provide(MyService, {useValue: service}),
            ])
            .createAsync(FakeComponent)
            .then((fixture:ComponentFixture<FakeComponent>) => {
                _fixture = fixture;
            });
    })));

    it('Shows loading while fetching data', (cb) => {
        // Make the call to getData take one ms so we can verify its state while the request is pending
        // Error occurs here, when the widget is initialized and sends out an XHR
        spyOn(service, 'getData').and.returnValue(Observable.of('value').delay(1));
        _fixture.detectChanges();
        expect(_fixture.nativeElement.querySelector('.loader')).toBeTruthy();
        // Wait a few ms, should not be loading
        // This doesn't seem to be the problem
        setTimeout(() => {
            _fixture.detectChanges();
            expect(_fixture.nativeElement.querySelector('.loader')).toBeFalsy();
            cb();
        }, 10);
    });
});

This was working fine in Angular2 rc1 and it throws an error in rc4, any suggestions?

Also, there are no errors if you use a setTimeout directly from the test itself

        fit('lets you run timeouts', async(() => {
            setTimeout(() => {
                expect(1).toBe(1);
            }, 10);
        }));
Ruan Mendes
  • 90,375
  • 31
  • 153
  • 217

2 Answers2

3

I've found that for some reason, you cannot use a promise created with Observable.of(anything).delay() in a test.

My solution for this was to implement that line myself, which almost makes sense considering the other example posted in the question did work.

// This is what we should be doing, but somehow, it isn't working.
// return Observable.of(result).delay(0));
function createDelayedObservable <T>(result:any, time:number = 0):Observable<T> {
    return new Observable<T>(observer => {
        setTimeout(() =>  observer.next(result), time);
    });
}

However, I still don't understand why the following doesn't fail, so I'm not accepting my own answer in the hopes that someone with a solid understanding of zones can tell me what's happening.

it('should be able to use delay in tests', (cb) => {
    var obs = Observable.of(1).delay(0);
    obs.subscribe(val => {
        expect(val).toBe(1);
        cb()
    });
});
Ruan Mendes
  • 90,375
  • 31
  • 153
  • 217
  • I have a similar problem i want to test a component that contains a `this.scroll = Observable.fromEvent(document.querySelector('nav'), 'scroll').debounceTime(100).subscribe((event) => { this.check(); });` but since it's a component I have not idea where I would place the callback – select Dec 22 '16 at 19:04
2

I've encountered the same issue. And I was able to workaround it using the jasmine done parameter.

fit('lets you run timeouts', (done) => {
    async(() => {
        setTimeout(() => {
            expect(1).toBe(1);
            done();
        }, 10);
    });
});
dkamburov
  • 396
  • 1
  • 7
  • Thanks, but the example you copied doesn't cause the problem for me. – Ruan Mendes Jul 26 '16 at 17:36
  • Also, adding a callback to the first example in my question doesn't solve the problem. That's how I actually had it earlier, before I found out how async worked. – Ruan Mendes Jul 26 '16 at 17:54