Manu Chadha
Manu Chadha

Reputation: 16747

RouterTestingModule not providing provider for Location

I am unit-testing a Component which uses two other components. The other components are created when button is clicked

.html

    <div id="homepage-top-div" class="homepage-component-css-grid-container">  ...
        <button id="get-question-list-button" [routerLink]="questionListRouterLink" class="btn content-div__button--blue css-grid-item-button-div btn-sm">See Questions</button>
      </div>
      <div id="practice-component-top-div" class="css-grid-container-div common-styles-div--white"> <!-- 4 rows, 1 column-->
...
        <button id="new-question-button" [routerLink]="newQuestionRouterLink"  class="btn content-div__button--blue css-grid-item-button-div btn-sm">Create a Question</button>
      </div>
    </div>

.ts

export class HomepageContentComponentComponent implements OnInit {

  public questionListRouterLink="/practice-question-list";
  public newQuestionRouterLink="/new-practice-question";


  constructor() {

  }

  ngOnInit() {
  }

}

I created a spec but I get the following error when I run it - Error: StaticInjectorError[Location]: NullInjectorError: No provider for Location!

The spec is

import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import { HomepageContentComponentComponent } from './homepage-content-component.component';
import {RouterTestingModule} from "@angular/router/testing";
import {AppRoutingModule} from "../app-routing.module";
import {Router} from "@angular/router";
import {routes} from '../app-routing.module';
import {NewPracticeQuestionComponent} from "../new-practice-question/new-practice-question.component";
import {PraticeQuestionListComponent} from "../pratice-question-list/pratice-question-list.component";
import {NgModule} from "@angular/core";
import {AppModule} from "../app.module";

fdescribe('HomepageContentComponentComponent', () => {
  let component: HomepageContentComponentComponent;
  let fixture: ComponentFixture<HomepageContentComponentComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports:[
        AppModule,
        AppRoutingModule,
        RouterTestingModule.withRoutes(routes) //<-- I SUPPOSE THIS STEP SHOULD PROVIDE ROUTERTESTINGMODULE WITH PROVIDERS FOR LOCATION BUT IT SEEMS IT ISN'T
      ],
      declarations: [ HomepageContentComponentComponent,
                  NewPracticeQuestionComponent,
        PraticeQuestionListComponent]
    })
    .compileComponents();
  }));

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

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should navigate to New Questions Component when New Question button is clicked',fakeAsync(()=>{
    let router:Router;
    let location:Location;
    router = TestBed.get(Router);
    location = TestBed.get(Location);
    console.log('initial router is ',router);
    console.log('initial location is ',location);
    router.initialNavigation();
    router.navigate(['new-practice-question']);
    tick();
    console.log('new router is ',router);
    console.log('new location is ',location);

    expect(location.pathname).toBe('/new-practice-question');
  }));
});

Upvotes: 4

Views: 6075

Answers (2)

Daniel Macak
Daniel Macak

Reputation: 17103

The reason why the wrong Location was being picked and why import {Location} from "@angular/common"; helped is that without importing it, TypeScript actually thinks you want to use the type of window.location, which is completely different type than the one you are looking for.

As the type is a token to retrieve desired instance from angular's DI registry, you don't get anything if you use a wrong type. When you comment out the import and 'click through' the Location type in your IDE, you will see that the picked type is actually a DOM interface, not the Angular implementation. This DOM interface is implicitly there once you have "lib": ["dom"] in your tsconfig.json.

Importing Location from angular explicitly overshadows Location from DOM, the right type is picked and the right Location instance is found in angular's DI registry.

It is actually pretty easy mistake to make (I made it myself), because even if you forget to import the Location, the DOM Location is picked and you get no feedback in your IDE at all and you are left wondering, why the DI fails.

Upvotes: 14

Manu Chadha
Manu Chadha

Reputation: 16747

Earlier, I made the code work by explicitly adding the providers. But I believe that shouldn't be the case. I was not sure why only using the RouterTestingModule didn't work.

Workaround

providers:[...,
{provide: Location, useClass: SpyLocation},
        {provide: LocationStrategy, useClass: MockLocationStrategy},
        {provide: NgModuleFactoryLoader, useClass: SpyNgModuleFactoryLoader}]

The actual issue was that the wrong definition of Location was being picked for unknown reason. The test worked without explicitly adding providers when I added import {Location} from "@angular/common";

Upvotes: 3

Related Questions