Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular unit test :WARN: 'Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?'

I am writing unit test for Angular app that deletes account from database. To do this, I click on delete button. Then function is called on .ts file. This will delete the account by calling API.

I want to write unit test to see if this API is called or not using HttpTestingModule,which it does, but in the code after account deleted,I navigate to different page using router.navigate. When code hits it complains WARN: 'Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?'

ERROR: 'Unhandled Promise rejection:', 'Cannot match any routes. URL Segment: 'accountsList'', '; Zone:', 'ProxyZone', '; Task:', 'Promise.then', '; Value:', Error: Cannot match any routes. URL Segment: 'accountsList'
Error: Cannot match any routes. URL Segment: 'accountsList'
    at ApplyRedirects.noMatchError (./node_modules/@angular/router/fesm5/router.js?:1455:16)
    at CatchSubscriber.eval [as selector] (./node_modules/@angular/router/fesm5/router.js?:1436:29)
    at CatchSubscriber.error (./node_modules/rxjs/_esm5/internal/operators/catchError.js?:40:31)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at ThrowIfEmptySubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26), 'Error: Cannot match any routes. URL Segment: 'accountsList'
    at ApplyRedirects.noMatchError (./node_modules/@angular/router/fesm5/router.js?:1455:16)
    at CatchSubscriber.eval [as selector] (./node_modules/@angular/router/fesm5/router.js?:1436:29)
    at CatchSubscriber.error (./node_modules/rxjs/_esm5/internal/operators/catchError.js?:40:31)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at ThrowIfEmptySubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)'

here is what I tried

test.spec.ts

 it ('should delete account, if account exist', ()=> {
    let spyOnDelete = spyOn (component,'deleteRecord').and.callThrough();
    fixture.detectChanges();
    component.record.accountid = "Account1";
    fixture.detectChanges();
    let deleteButtonDOM = fixture.debugElement.query(By.css('#deletebtn'));
    console.log(deleteButtonDOM.nativeElement);
    deleteButtonDOM.triggerEventHandler('click',null);
    fixture.detectChanges();
    expect(spyOnDelete).toHaveBeenCalled();  //test passes 

    const req = _HttpTestingController.expectOne('/api/accounts/'+component.record.accountid);//test fails
    expect(req.request.method).toBe("DELETE");
    req.flush({status:"SUCCESS"});
    expect(component.consoleMessages.includes("POST: SUCCESS in /api/accounts")).toBe(true);


..what must be written to 
  })

test.component.ts

  deleteRecord(id) {
    this.spinner.show();
    this.http.delete('/api/accounts/' + id)
    .subscribe(res => {
        this.spinner.hide();
        if (res['status'] == "FAILURE") {
          this.consoleMessages += "\nPOST: ERROR in /api/accounts\n" + JSON.stringify(res);  
        } else {
          this.consoleMessages += "\nPOST: SUCCESS in /api/accounts\n" + JSON.stringify(res);
          this.router.navigate(['/accountsList']);//this is creating the problem xxxxxxxxxxxxxxxx
        }
      }, (err) => {
        this.consoleMessages += "\nPOST: ERROR in /api/accounts\n" + JSON.stringify(err); 
        this.spinner.hide();
        console.log(err);
      }
    );
  }
like image 617
karansys Avatar asked Oct 03 '19 13:10

karansys


1 Answers

You should mock your router.

The way I do it with RouterTestingModule. Here's the set up:

beforeEach(async(() => {
  TestBed.configureTestingModule({
    imports: [
      RouterTestingModule.withRoutes([
        { path: "", component: AccountComponent },
        { path: "**", redirectTo: "" }
      ])
    ],
    declarations: [AccountComponent],
    providers: []
  }).compileComponents();
}));

beforeEach(() => {
  fixture = TestBed.createComponent(AccountComponent);
  component = fixture.componentInstance;
  router = TestBed.inject(Router);
  fixture.detectChanges();
});

Then in your test:

it('THEN: should navigate to /accounts', () => {
      const ID_TO_DELETE = 1;
      const routerNavigateSpy = jest
        .spyOn(router, 'navigate')
        .mockImplementation(() => of(true).toPromise());
      component.deleteRecord(ID_TO_DELETE);
      expect(routerNavigateSpy).toHaveBeenCalledWith(['/accounts']);
});

Bonus

This post explains how Angular zones work: ngZone

like image 126
adrisons Avatar answered Sep 28 '22 15:09

adrisons