New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error: Cannot make XHRs from within a fake async test. #8280

Closed
andrashatvani opened this Issue Apr 27, 2016 · 20 comments

Comments

Projects
None yet
@andrashatvani

andrashatvani commented Apr 27, 2016

Steps to reproduce and a minimal demo of the problem

What steps should we try in your demo to see the problem?

export function setupComponentFakeAsync(type:any, continuation:Function) {
    return fakeAsync(inject([TestComponentBuilder], (tcb:TestComponentBuilder) => {
        let fixture:ComponentFixture = null;
        tcb.createAsync(type).then((f:ComponentFixture) => fixture = f);
        tick(50);
        fixture.detectChanges();
        continuation(fixture);
    }));
}


it('shows an empty origin station text field in the default component state',
    setupComponentFakeAsync(SearchComponent, (fixture:ComponentFixture) => {
        let input:any = fixture.nativeElement.querySelector('#fromStation');

        expect(input.value).toBe('');
    })
);



**Current behavior**
Exception:

> Error: Cannot make XHRs from within a fake async test. in d:/Users/l026291/Development/ts-automat-fe/node_modules/zone.js/dist/fake-async-test.js (line 248)
    onScheduleTask@d:/Users/l026291/Development/ts-automat-fe/node_modules/zone.js/dist/fake-async-test.js:248:96
    scheduleTask@d:/Users/l026291/Development/ts-automat-fe/node_modules/angular2/bundles/angular2-polyfills.js:359:64
    scheduleMacroTask@d:/Users/l026291/Development/ts-automat-fe/node_modules/angular2/bundles/angular2-polyfills.js:299:52
    d:/Users/l026291/Development/ts-automat-fe/node_modules/angular2/bundles/angular2-polyfills.js:148:39
    send

**Expected/desired behavior**
The test is working just as with beta 15.

**Other information**
@aprinaresh97

This comment has been minimized.

aprinaresh97 commented May 20, 2016

We are also running into the same issue. Is there a solution for this. All our unittests are failing because of this issue. I assume the XHR is used to load the styleUrl. Is there a workaround for this. Due to this issue, we are not able to close any story because of failure to meet the acceptance criteria.

Let us know how to proceed.

@aprinaresh97

This comment has been minimized.

aprinaresh97 commented May 20, 2016

To make some of the tests passing, I had to remove the fakeAsync and skip tests that uses tick (as tick works only in the fakeAsyncZone).

What are our options? Is this going to be fixed soon?

@simo14

This comment has been minimized.

simo14 commented May 22, 2016

Same here. If I use async to inject the component karma fails. If I use fakeAsync I get this exception.

@onlyann

This comment has been minimized.

onlyann commented May 25, 2016

This issue is also preventing us from unit testing any component that makes uses of setTimeout or the delay Rxjs operator.

@roni-frantchi

This comment has been minimized.

roni-frantchi commented Jun 9, 2016

+1 here...

@yjaaidi

This comment has been minimized.

Contributor

yjaaidi commented Jun 17, 2016

+1

@yjaaidi

This comment has been minimized.

Contributor

yjaaidi commented Jun 17, 2016

I guess I know where it comes from!
Are you guys using templateUrl ?
Because I have mocked the Http service and still got the error until I replaced templateUrl with template.

We should find a way to override all component templates.

@zoechi

This comment has been minimized.

Contributor

zoechi commented Jun 20, 2016

@yjaaidi what Angular2 version?

@yjaaidi

This comment has been minimized.

Contributor

yjaaidi commented Jun 21, 2016

Hi @zoechi,

It's rc1. Didn't try with rc2 yet.

@juliemr

This comment has been minimized.

Member

juliemr commented Jun 21, 2016

This is working as intended - we can't "fake" async behavior in any reasonable way if a real XHR is made, so the test must throw an error. templateUrl and styleUrl make XHRs.

Something that we can do, however, is run tests against a cached version of the templates and styles, so we don't need to make the XHR in the first place. @vikerman worked on a system to do this, see #7940

There is little documentation though - we should add an example of a test setup using this. @vikerman does one exist that I don't know about, or should I create one?

@vicb

This comment has been minimized.

Contributor

vicb commented Jun 21, 2016

Should that be in a cookbook on angular.io ? If you think this makes sense could you please create an issue with the detailson the angular.io repo. Thanks.

@centaure

This comment has been minimized.

centaure commented Jun 23, 2016

Below is a spec example for a component using templateUrl (adapted from here). I used rc3 for my impl.

However, the templateUrl is being mapped to its content string within setTemplateCache below (i.e. content of ./application.html is still being inlined).
Is there a straightforward way to read from the actual ./application.html file in this example (Note that I am using systemjs for loading modules)?

 import { 
   ComponentFixture, TestComponentBuilder
 } from '@angular/compiler/testing';
 import { Component } from '@angular/core';
 import {
   beforeEachProviders,
   describe,
   expect,
   fakeAsync,
   inject,
   it,
   tick
 } from '@angular/core/testing';
 import { UrlResolver, XHR } from '@angular/compiler';
 import { 
   CachedXHR
 } from '@angular/platform-browser-dynamic/src/xhr/xhr_cache';

 describe('ApplicationComponent fixture', () => {
   function createCachedXHR(): CachedXHR {
     setTemplateCache({ './application.html': '<div>Hello World</div>' });
     return new CachedXHR();
   }
   beforeEachProviders(() => {
     return [
       { provide: UrlResolver, useClass: TestUrlResolver },
       { provide: XHR, useFactory: createCachedXHR }
     ];
   });

   it ('should have the correct content',
     fakeAsync(inject([TestComponentBuilder],
       (tcb: TestComponentBuilder) => {
         let fixture: ComponentFixture;

         tcb.createAsync(ApplicationComponent).then(
           (f) => { fixture = f; }
         );
         tick();
         expect(
           fixture.debugElement.children[0].nativeElement
         ).toHaveText('Hello World');
       }
     ))
   );
 });

 export function setTemplateCache(cache): void {
   (<any>window).$templateCache = cache;
 }

 class TestUrlResolver extends UrlResolver {
   resolve(baseUrl: string, url: string): string {
     // Don't use baseUrl to get the same URL as templateUrl.
     // This is to remove any difference between Dart and TS tests.
     return url;
   }
 }

 @Component({
   selector: 'app',
   templateUrl: './application.html'
 })
 export default class ApplicationComponent {}
@simonxca

This comment has been minimized.

simonxca commented Aug 3, 2016

@centaure Here's how I hacked my way through this:

  • Serve your .html and .css files. I'm using Karma so in my karma.conf.js I put:
files: [
  ...
  {
    pattern: 'public/templates/**/*.html',
    included: false,
    served: true
  },
  {
    pattern: 'public/css/**/*.css',
    included: false,
    served: true
  },
  ...
]
  1. Define window.$templateCache = {} at the top.
  2. Populate the template cache:
// load your test files
}).then(function () {
  ...

// GET .html and .css templates and cache them
}).then(function () {

  return Promise.all(
    Object.keys(window.__karma__.files)

    .filter(function (filename) {
      var isTemplate = filename.startsWith('/base/public/templates') && filename.endsWith('.html');
      var isCss = filename.startsWith('/base/public/css') && filename.endsWith('.css');
      return isTemplate || isCss;
    })

    .map(function (filename) {
      return new Promise((resolve, reject) => {
        $.ajax({
          type: 'GET',
          url: filename,
          dataType: 'text',
          success: function (contents) {
            filename = filename.replace('/base/public/', '');
            window.$templateCache[filename] = contents;
            resolve();
          }
        });
      });
    })
  );

// start karma
}).then(function () {
  ...
});

As you can see, I make a GET request for the contents of the files I want and just save them in the window.$templateCache object, which will be loaded later by CachedXHR.

@gridcellcoder

This comment has been minimized.

gridcellcoder commented Sep 28, 2016

@juliemr @vikerman do you guys have an example of using the cache to load template url contents?

@mattxo

This comment has been minimized.

mattxo commented Nov 17, 2016

Hm, same problem here...

    it('should show already exists message when save button clicked', fakeAsync(() => 
    {                     
        Promise.all
        ([
            page.sendInput(page.firstNameInput, 'Matt'),
            page.sendInput(page.lastNameInput, 'Sample'),
            page.sendInput(page.emailInput, 'me@hotmail.com'),
            page.sendInput(page.passwordInput, 'abcDEF123'),
        ])
        .then(() => 
        {            
            page.form.submit();                                    
            tick();
            expect(component.errorMessage).toContain('already exists');                                                                                                                                
        })         
    }));

Where page.form.submit() simply calls register() and users.register() is hitting a local unit test database...

    register(event : any)
    {                        
        event.preventDefault();

        return this.users.register(this.model)            
            .then(user => this.onSuccess(user))
            .catch(error => this.onError(error));        
    }

@yjaaidi

This comment has been minimized.

Contributor

yjaaidi commented Nov 17, 2016

Hello @mattxo ,

Oh! That's normal. As your app is making an http request, fakeAsync can't fake http.

You should mock the http backend. It's not as fun as AngularJS 1.x $httpBackend but here's a batteries included pattern.

    beforeEach((done) => {

        TestBed
            .configureTestingModule({
                imports: [
                    UserModule
                ],
                providers: [
                    MockBackend,
                    {
                        provide: Http,
                        deps: [MockBackend, RequestOptions],
                        useFactory: (mockBackend, requestOptions) => {
                            return new Http(mockBackend, requestOptions);
                        }
                    }
                ]
            })
            .compileComponents()
            .then(done);

    });

    beforeEach(inject([MockBackend], (mockBackend: MockBackend) => {

        this.spyConnection = jasmine.createSpy('connection');

        mockBackend.connections.subscribe((connection: MockConnection) => {
            connection.mockRespond(new Response(new ResponseOptions(this.spyConnection({
                body: connection.request.text(),
                method: connection.request.method,
                url: connection.request.url
            }))));
        });

    }));

    it(..., () => {
        this.spyConnection.and.returnValue({
            body: { /* YOUR EXPECTED RESPONSE HERE */ },
            status: 200
        });

        expect(this.spyConnection).toHaveBeenCalledTimes(1);
        expect(this.spyConnection.calls.argsFor(0)[0].method).toEqual(RequestMethod.Get);
        expect(this.spyConnection.calls.argsFor(0)[0].url).toEqual('/api/v1/endpoint.../');

    });
@mattxo

This comment has been minimized.

mattxo commented Nov 18, 2016

Hey @yjaaidi,

Thanks for the sample code :)

I'd prefer not mock the backend in this case though, if I can help it. It's an e2e/integration test, and performance isn't an issue with a local database.

The fakeAsync in this case is only there to make this work

form.submit();
tick(); // wait until all promises resolve

What I'd really like, if it's not possible to have the XHR run synchronously when called under fakeAsync, is to have the register() promise returned from form.submit() or possibly better still, from page.submitButton.click(), but that doesn't seem possible either...

@Olgagr

This comment has been minimized.

Olgagr commented Oct 26, 2017

We had the same error. For us the key thing was setup providers correctly in TestBed.configureTestingModule.

let lastConnection: any;
let component: InvoiceFormComponent;

beforeEach(() => {
TestBed.configureTestingModule({
    imports: [...],
    providers: [
        {
          provide: ConnectionBackend,
          useClass: MockBackend,
        },
        {
          provide: RequestOptions,
          useClass: BaseRequestOptions,
        },
        Http, // DON'T FORGET TO ADD THIS! 
    ],
    declarations: [MyComponent],
}).compileComponents();
    let backend = TestBed.get(ConnectionBackend) as MockBackend;
    backend.connections.subscribe((connection: any) => { lastConnection = connection; });
));

Then in the test:

it('getWorkItemsOptions', fakeAsync(() => {
    let options;
    componentFixture = TestBed.createComponent(MyComponent);
    component = componentFixture.componentInstance;

    component.someAsyncAction().subscribe((result) => {
        options = result;
    });

   lastConnection.mockRespond(new Response(new ResponseOptions({
       body: JSON.stringify([{ name: 'one' }, { name: 'two' }]),
       status: 200,
    })));
   tick();
   expect(options.length).toEqual(2);
}));
@vikerman

This comment has been minimized.

Contributor

vikerman commented Jan 26, 2018

Example should be covered as part of resolving #12409

@vikerman vikerman closed this Jan 26, 2018

@Sampath-Lokuge

This comment has been minimized.

Sampath-Lokuge commented Aug 25, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment