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

3

8

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);
      }
    );
  }
Becerra answered 3/10, 2019 at 13:29 Comment(4)
Try declaring your spy in the describe function, and make sure that it is added to your test bed configWarhead
Hi Rinktacular spy to which function?Becerra
Answer to my question is here #39792273Becerra
Answer to my question is here #39792273Becerra
G
11

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

Genaro answered 16/3, 2020 at 13:36 Comment(0)
A
2

Minimal spy and solution for the warning router.navigate

describe('Component', () => {
  ... other variables
  let router: jasmine.SpyObj<Router>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule.withRoutes([yourPaths])
      ],
      ...other declarations
    })
      .compileComponents();
  }));

  beforeEach(() => {
    ...other initializing stuffs
    router = TestBed.get(Router);
  });

  it('should navigate', () => {
     spyOn(router, 'navigate');
     component.navigateToRoute();
     expect(router.navigate).toHaveBeenCalledWith(['/expected-path']);
  });
});
Armor answered 21/10, 2020 at 5:31 Comment(0)
G
0

In my case mocking router was not enough. I needed to spy on the router as well.

beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule.withRoutes(
          [{ path: 'your-path', redirectTo: '' }]
        )]
    }).compileComponents();
}));

beforeEach(() => {
    fixture = TestBed.createComponent(YourComponent);
    component = fixture.componentInstance;
    snackBarService = TestBed.inject(SnackBarService);
    spyOn(component['router'], 'navigate'); // spying on router
    fixture.detectChanges();
});
... tests
Gosselin answered 28/5 at 19:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.