1

I am getting Failed: Uncaught (in promise): Error: Cannot find primary outlet to load 'UserList' error.

This happens when unit testing my login component that redirects to admin/userlist using this._router.navigate(['admin/userlist']); in the component.

login.html:

<div class="row registration">
  <div id="login-form-control" class="col-xs-12 form-area">
    <h2 class="registration-header">Login</h2>
    <form (ngSubmit)="login()" [formGroup]="form">
      <input id="login-username-textbox" type="text" class="form-control" placeholder="Username" [formControl]="username" required>
      <input id="login-password-textbox" type="password" class="form-control" placeholder="Password" [formControl]="password" required>
      <div *ngIf="errorMessage">
        <span class="help-block error">{{errorMessage}}</span>
      </div>
      <div *ngIf="successMessage">
        <span class="help-block success">{{successMessage}}</span>
      </div>
      <button id="login-submit" type="submit" class="go-btn btn btn-lg btn-success btn-block">Login</button>
    </form>
    <div class="row">
      <a [routerLink]="['../register']">Register</a>
    </div>    
  </div>
</div>

login.ts:

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';

import { RegistrationService } from '../shared/services/registration';

@Component({
  selector: 'rg-login',
  templateUrl: './login.html'
})
export class Login {
  form: FormGroup;
  username = new FormControl('', Validators.required);
  password = new FormControl('', Validators.required);

  errorMessage = '';
  successMessage = '';

  constructor(private _registrationService: RegistrationService, private _formBuilder: FormBuilder, private _router: Router) {
    this._createForm();
  }

  ngOnInit() {
  }

  login() {
    this._registrationService.loginUser(this.form.value)
        .subscribe(data => {
          if (data) {
            this.errorMessage = '';
            this.successMessage = 'Login successful';
            this._router.navigate(['admin/userlist']);
          } else {
            this.errorMessage = 'Error';
            this.successMessage = '';
          }
        }, error => {
          this.errorMessage = error;
          this.successMessage = '';
        });
  }

  _createForm() {
    this.form = this._formBuilder.group({
      username:  this.username,
      password: this.password
    });
  }
}

login.spec.ts:

import { Component } from '@angular/core';
import { async, inject, TestBed } from '@angular/core/testing';
import { BaseRequestOptions, ConnectionBackend, Http } from '@angular/http';
import { MockBackend } from '@angular/http/testing';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { Observable } from 'rxjs/Rx';

import { RegistrationService } from '../shared/services/registration';
import { Login } from './login';

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

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

class MockRegistrationService {
  loginUser(user) {
    return Observable.of({
      username: 'TestUser1',
      password: 'TestPassword1'
    });
  }
}

describe('Login', () => {
  let mockRegistrationService = new MockRegistrationService();

  beforeEach(() => TestBed.configureTestingModule({
    declarations: [
      Login,
      Register,
      UserList
    ],
    providers: [
      Login,
      { provide: RegistrationService, useValue: mockRegistrationService }
    ],
    imports: [
      ReactiveFormsModule,
      RouterTestingModule.withRoutes([
        { path: 'register', component: Register },
        { path: 'admin/userlist', component: UserList }
      ])
    ]
  }));

  it('should successfully login', async(() => {
    let fixture = TestBed.createComponent(Login);
    let loginComponent = fixture.componentInstance;

    fixture.detectChanges();

    loginComponent.login({
      username: 'TestUser1',
      password: 'TestPassword1'
    });

    expect(loginComponent.successMessage).toEqual('Login successful');
    expect(loginComponent.errorMessage).toEqual('');
  }));

});

Full error:

FAILED TESTS:
  Login
    ✖ should successfully login
      PhantomJS 2.1.1 (Mac OS X 0.0.0)
    Failed: Uncaught (in promise): Error: Cannot find primary outlet to load 'UserList'
    resolvePromise@webpack:///~/zone.js/dist/zone.js:429:0 <- config/spec-bundle.js:53943:75
    webpack:///~/zone.js/dist/zone.js:406:0 <- config/spec-bundle.js:53920:27
    invoke@webpack:///~/zone.js/dist/zone.js:203:0 <- config/spec-bundle.js:53717:33
    onInvoke@webpack:///~/zone.js/dist/async-test.js:42:0 <- config/spec-bundle.js:52757:45
    onInvoke@webpack:///~/zone.js/dist/proxy.js:69:0 <- config/spec-bundle.js:53414:47
    invoke@webpack:///~/zone.js/dist/zone.js:202:0 <- config/spec-bundle.js:53716:42
    run@webpack:///~/zone.js/dist/zone.js:96:0 <- config/spec-bundle.js:53610:49
    webpack:///~/zone.js/dist/zone.js:462:0 <- config/spec-bundle.js:53976:60
    invokeTask@webpack:///~/zone.js/dist/zone.js:236:0 <- config/spec-bundle.js:53750:42
    onInvokeTask@webpack:///~/zone.js/dist/proxy.js:96:0 <- config/spec-bundle.js:53441:49
    invokeTask@webpack:///~/zone.js/dist/zone.js:235:0 <- config/spec-bundle.js:53749:54
    runTask@webpack:///~/zone.js/dist/zone.js:136:0 <- config/spec-bundle.js:53650:57
    drainMicroTaskQueue@webpack:///~/zone.js/dist/zone.js:368:0 <- config/spec-bundle.js:53882:42
    invoke@webpack:///~/zone.js/dist/zone.js:308:0 <- config/spec-bundle.js:53822:44
xphong
  • 1,114
  • 2
  • 13
  • 20
  • Does the `Login` component template have a ``? If not just add a dummy component with one for the test. The error is saying that there's no outlet for the routed component(s) – Paul Samsotha Nov 29 '16 at 14:22
  • @peeskillet There is no in the login template. – xphong Nov 29 '16 at 14:25
  • Just add a dummy component that has one. – Paul Samsotha Nov 29 '16 at 14:28
  • Or better yet just use a router stub for the test. And just check that the navigate method is called. This way there is not need to configure any routes. No need to test any real navigation, unless that's what you really want. – Paul Samsotha Nov 29 '16 at 14:30
  • @peeskillet Can you explain how to correctly create the router stub or dummy component? I tried adding `` to the dummy template of UserList Component. Also tried creating a new dummy Router component with `` in its template. – xphong Nov 29 '16 at 15:06

1 Answers1

2

Can you explain how to correctly create the router stub

Just do

let routerStub;

beforeEach(() => {
  routerStub = {
    navigate: jasmine.createSpy("navigate")
  };
  TestBed.configureTestingModule({
    providers: [
      // add router provider
      { provide: Router, useValue: routerStub },
      { provide: RegistrationService, useValue: mockRegistrationService }
    ],
    imports: [
      ReactiveFormsModule,
      // Remove Router Module
    ]
  })
});

Then in your test, just check that the navigate method is called with the correct arguments

expect(routerStub.navigate).toHaveBeenCalledWith(['someurl'])

This is more what a unit test should look like. You just want to test the behavior of the component. So you just check that it calls the navigate method on the router.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720