Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 how do I test a component that uses router

I am trying to write some tests for a component that calls router.navigate() and I'm stuck on errors with declaring the routes. I've read a lot of things and tried all of them but they all lead to some error or another. I am using Angular 4.0.0.

Component:

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
  constructor(
    private activatedRoute: ActivatedRoute,
    private authService: AuthService,
    private formBuilder: FormBuilder,
    private jwtService: JwtService,
    private router: Router,
    private storageService: StorageService
  ) { ... }

  ngOnInit() {
  }

  private login(formData: any): void {
    const credentials: any = {
      email: formData.controls.email.value,
      password: formData.controls.password.value
    };
    this.authService.login(credentials).subscribe(res => {
      this.activatedRoute.params.subscribe(params => {
        if (params.returnUrl) {
          this.router.navigate([params.returnUrl]);
        } else {
          this.router.navigate(['/dashboard']);
        }
      });
    }, error => { ... });
  }
}

Test:

describe('LoginComponent', () => {
  let component: any;
  let fixture: ComponentFixture<LoginComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ LoginComponent ],
      imports: [
        SharedModule,
        RouterTestingModule
      ],
      providers: [{
        provide: AuthService,
        useClass: MockAuthService
      }, {
        provide: JwtService,
        useClass: MockJwtService
      }, {
        provide: StorageService,
        useClass: MockStorageService
      }],
      schemas: [ NO_ERRORS_SCHEMA ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  describe('login', () => {
    it('should call router.navigate with the dashboard route if the login is successful', () => {
      spyOn(component.router, 'navigate');
      component.authService.login.and.returnValue(Observable.of({ access_token: 'fake_token' }));
      component.login(component.loginForm);
      expect(component.router.navigate).toHaveBeenCalledWith(['/dashboard']);
    });
  });
});

This all gives me the following error:

zone.js:569 Unhandled Promise rejection: Cannot match any routes. URL Segment: 'dashboard'

So from there I looked into adding the route with withRoutes. I don't like that I need to include the DashboardComponent since it seems like there should be some mocked/blank component available for this especially since I don't want to actually navigate and load another route but I couldn't find anything like that:

TestBed.configureTestingModule({
  declarations: [ LoginComponent ],
  imports: [
    SharedModule,
    RouterTestingModule.withRoutes([{
      path: 'dashboard',
      component: DashboardComponent
    }])
  ],
  ...
})
.compileComponents();

However that just give me a new error:

Component DashboardComponent is not part of any NgModule or the module has not been imported into your module.

So I thought that maybe I needed to declare the DashboardComponent, so I added it to the declarations array:

TestBed.configureTestingModule({
  declarations: [ LoginComponent, DashboardComponent ],
  ..
})
.compileComponents();

However that just lead to yet another error:

Unhandled Promise rejection: Cannot find primary outlet to load 'DashboardComponent'

At this point it seems like there must be a simpler way to do this as it's a very common scenario but I've tried everything others say they used and everything just leads further down this rabbit hole.

like image 498
efarley Avatar asked Mar 10 '23 03:03

efarley


1 Answers

The solution turned out to be really simple...

Simply adding the RouterTestingModule was almost there, only I needed to spy on router.navigate in all tests to prevent them from trying to actually navigate to another route.

describe('LoginComponent', () => {
  let component: any;
  let fixture: ComponentFixture<LoginComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ LoginComponent ],
      imports: [
        SharedModule,
        RouterTestingModule   // This provides the mock router, location and routerLink
      ],
      providers: [{
        provide: AuthService,
        useClass: MockAuthService
      }, {
        provide: JwtService,
        useClass: MockJwtService
      }, {
        provide: StorageService,
        useClass: MockStorageService
      }],
      schemas: [ NO_ERRORS_SCHEMA ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    spyOn(component.router, 'navigate');    // This prevents every test from calling the real router.navigate which means I don't need to add routes to RouterTestingModule
  });

  describe('login', () => {
    it('should call router.navigate with the dashboard route if the login is successful', () => {
      spyOn(component.router, 'navigate');
      component.authService.login.and.returnValue(Observable.of({ access_token: 'fake_token' }));
      component.login(component.loginForm);
      expect(component.router.navigate).toHaveBeenCalledWith(['/dashboard']);
    });
  });
});
like image 155
efarley Avatar answered Mar 24 '23 15:03

efarley