71

I am trying to test my component with angular 2 final, but I get an error because the component uses the routerLink directive. I get the following error:

Can't bind to 'routerLink' since it isn't a known property of 'a'.

This is the relevant code of the ListComponent template

<a 
  *ngFor="let item of data.list" 
  class="box"
  routerLink="/settings/{{collectionName}}/edit/{{item._id}}">

And here is my test.

import { TestBed } from '@angular/core/testing';

import { ListComponent } from './list.component';
import { defaultData, collectionName } from '../../config';
import { initialState } from '../../reducers/reducer';


const data = {
  sort: initialState.sort,
  list: [defaultData, defaultData],
};

describe(`${collectionName} ListComponent`, () => {
  let fixture;
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [
        ListComponent,
      ],
    }).compileComponents(); // compile template and css;
    fixture = TestBed.createComponent(ListComponent);
    fixture.componentInstance.data = data;
    fixture.detectChanges();
  });

  it('should render 2 items in list', () => {
    const el = fixture.debugElement.nativeElement;
    expect(el.querySelectorAll('.box').length).toBe(3);
  });
});

I looked at several answers to similar questions but could not find a solution that worked for me.

4 Answers 4

130

You need to configure all the routing. For testing, rather than using the RouterModule, you can use the RouterTestingModule from @angular/router/testing, where you can set up some mock routes. You will also need to import the CommonModule from @angular/common for your *ngFor. Below is a complete passing test

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { By } from '@angular/platform-browser';
import { Location, CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { TestBed, inject, async } from '@angular/core/testing';

@Component({
  template: `
    <a routerLink="/settings/{{collName}}/edit/{{item._id}}">link</a>
    <router-outlet></router-outlet>
  `
})
class TestComponent {
  collName = 'testing';
  item = {
    _id: 1
  };
}

@Component({
  template: ''
})
class DummyComponent {
}

describe('component: TestComponent', function () {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        CommonModule,
        RouterTestingModule.withRoutes([
         { path: 'settings/:collection/edit/:item', component: DummyComponent }
        ])
      ],
      declarations: [ TestComponent, DummyComponent ]
    });
  });

  it('should go to url',
    async(inject([Router, Location], (router: Router, location: Location) => {

    let fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();

    fixture.debugElement.query(By.css('a')).nativeElement.click();
    fixture.whenStable().then(() => {
      expect(location.path()).toEqual('/settings/testing/edit/1');
      console.log('after expect');
    });
  })));
});

UPDATE

Another option, if you just want to test that the routes are rendered correctly, without trying to navigate...

You an just import the RouterTestingModule without configuring any routes

imports: [ RouterTestingModule ]

then just check that the link is rendered with the correct URL path, e.g.

let href = fixture.debugElement.query(By.css('a')).nativeElement
    .getAttribute('href');
expect(href).toEqual('/settings/testing/edit/1');
Sign up to request clarification or add additional context in comments.

8 Comments

Thanks, this helps already alot, but I cannot get the unit test to fail. It seems that the because of async everything in this block is ginored. I tried to add a done function to the it('...', (done) => {async(... done())} but then I get Error: Timeout from the test.
My bad, I had the async wrapped in another function and not as second parameter of the it. It works now, thanks.
This is fantastic. Finally found an answer that applies to 2.0 Final (I'm using 2.1)! Since I was going to need the same configuration in a lot of tests, I created a test-only module that configured RouterTestingModule and declared the DummyComponent. Then I put that module and the DummyComponent into a test/ subfolder, which I can then import into any of my unit tests. Thanks @peeskillet
Actually, if you only need to render routerLink you don't have to setup the routing
getAttribute('href') returns null for me, while it works in app. Is there something I miss?
|
23

If you are not testing router related stuff, you can configure the test to ignore unknown directives with 'NO_ERRORS_SCHEMA'

 import { NO_ERRORS_SCHEMA } from '@angular/core';
 TestBed.configureTestingModule({
   declarations: [
     ListComponent,
   ],
   schemas: [ NO_ERRORS_SCHEMA ]
 });

4 Comments

Thanks that is even a more specific solution to my problem, thoug I learned also form the first one. Could you please also share where to import NO_ERRORS_SCHEMA from.
NO_ERRORS_SCHEMA is currently flagged as Experimental
@CalvinDale That shouldn't matter. A lot of things are marked experimental (including commonly used classes like Http). angular.io/docs/ts/latest/api/#!?status=experimental
You should pretty much never use NO_ERRORS_SCHEMA it does a lot more than just fix this. It also causes ALL errors to pass in tests
12

To write a test case for routerLink. You can follow the below steps.

  1. Import RouterTestingModule and RouterLinkWithHref.

    import { RouterTestingModule } from '@angular/router/testing';
    import { RouterLinkWithHref } from '@angular/router';
    
  2. Import RouterTestingModule in your module

    TestBed.configureTestingModule({
      imports: [ RouterTestingModule.withRoutes([])],
      declarations: [ TestingComponent ]
    })
    
  3. In test case find the directive RouterLinkWithHref tot test for the existence of the link.

    it('should have a link to /', () => {
      const debugElements = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
      const index = debugElements.findIndex(de => {
        return de.properties['href'] === '/';
      });
      expect(index).toBeGreaterThan(-1);
    });
    

2 Comments

The import work also as imports: [ RouterTestingModule ]
linkDes = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); routerLinks = linkDes.map(de=>de.injector.get(RouterLinkWithHref)); expect(routerLinks[0].href).toBe('/settings/testing/edit/1');
1

How to check that the Router actually triggered the URL change


Solution inspired by https://medium.com/@douglascaina/angular-tdd-how-to-test-routerlink-or-url-change-494f18208443


My setup (using ng-mocks):

class MockComponent {}

// ...

MockBuilder(MyComponent)
  .keep(RouterTestingModule.withRoutes([{ path: 'my/path', component: MockComponent }]))
  .keep(RouterLink)

My test case:

it('when clicked, myLinkElement should lead to /my/path', fakeAsync(() => {
  const { page, router } = setup();
  page.detectChanges();

  page.click(page.myLinkElement().nativeElement);
  tick();

  expect(router.url).toEqual('/my/path');
}));

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.