Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Running Angular2 tests that call setTimeout errors with "Cannot use setInterval from within an async zone test"

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);
        }));
like image 476
Juan Mendes Avatar asked Jul 25 '16 18:07

Juan Mendes


2 Answers

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()
    });
});
like image 158
Juan Mendes Avatar answered Sep 26 '22 05:09

Juan Mendes


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);
    });
});
like image 33
dkamburov Avatar answered Sep 23 '22 05:09

dkamburov