NRAOReid
NRAOReid

Reputation: 13

Unit test angular component with input binding and router links gives error 'Cannot read property '__source' of undefined'

I'm trying to write a unit test for a component with an @Input property that happens to include an anchor tag with a [routerLink] in it.

I was getting the error:

Failed: Template parse errors: Can't bind to 'routerLink' since it isn't a known property of 'a'.

To fix that, I added the RouterTestingModule and now get this error:

TypeError: Cannot read property '__source' of undefined

for every test.

Component.ts

import {Component, Input, OnInit} from '@angular/core';
import {Tile, TileDefinition} from "../../model/tile";
import {TilesService} from "../../services/tiles.service";
import {Observable} from "rxjs";

@Component({
  selector: 'app-tile',
  templateUrl: './tile.component.html',
  styleUrls: ['./tile.component.scss']
})
export class TileComponent implements OnInit {

  @Input() tile: Tile;

  percentComplete: string;

  detailsExposed: boolean = false;
  public tileDescription: Observable<TileDefinition>;


  constructor(private tileService: TilesService) { }

  ngOnInit() {
    let num = parseInt(this.tile.custom3);
    if(isNaN(num)){
      this.percentComplete = '0%';
    } else {
      this.percentComplete = parseInt(this.tile.custom3).toString() + '%';
    }
  }

  showDetails(): void {
    this.detailsExposed = true;
    this.tileDescription = this.tileService.getTileDefinition(this.tile.id);
  }

  hideDetails(): void {
    this.detailsExposed = false;
  }
}

Component.html

<div class="row no-gutters mx-1 py-2 border-bottom">
  <div class="col-1">{{ tile.id }}</div>
  <div class="col">{{ tile.name }}</div>
  <div class="col-2">
    <div class="form-row">
      <div class="col">
        <div class="progress">
          <div class="progress-bar"
               [ngClass]="{'bg-success': percentComplete == '100%'}"
               [ngStyle]="{'width': percentComplete}">
            {{percentComplete}}
          </div>
        </div>
      </div>

      <div class="col-auto">
        <button class="btn btn-xs btn-light border text-primary show_more_button" *ngIf="!detailsExposed" (click)="showDetails()">More
        </button>
        <button class="btn btn-xs btn-light border text-primary show_less_button" *ngIf="detailsExposed" (click)="hideDetails()">Less
        </button>
      </div>
    </div>
  </div>
</div>

<div class="p-3 mx-1 border rounded-bottom bg-white" *ngIf="detailsExposed">
  <ng-container *ngIf="tileDescription | async; else loading">

    <a [routerLink]="['/fileeditor','tile',tile.id,'json','definitions']" class="btn btn-sm btn-info">Edit Configuration</a>

  </ng-container>

  <ng-template #loading>
    <div class="col-12 text-center"><p class="display-4 text-muted">Loading</p></div>
  </ng-template>

</div>

Test Spec

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

import {TileComponent} from './tile.component';
import {TilesService} from "../../services/tiles.service";
import {Component, DebugElement, ViewChild} from "@angular/core";
import {Tile, TileDefinition} from "../../model/tile";
import {By} from "@angular/platform-browser";
import {RouterTestingModule} from "@angular/router/testing";
import {of} from "rxjs";

// mock a host container so we can input bind to the unit under test
@Component({
  template: '<app-tile [tile]="tile"></app-tile>'
})
class HostComponent{
  tile: Tile = {
    id: "5",
    name: "TileName",
    custom3: "20% imaged"
  };
  @ViewChild(TileComponent) tileComponent: TileComponent;
}

describe('TileComponent', () => {
  let component: HostComponent;
  let fixture: ComponentFixture<HostComponent>;
  let tilesServiceSpy: { getTileDefinition: jasmine.Spy }; // we'll use a spy in place of the real service

  beforeEach(async(() => {
    // create the spy to inject as a substitute for the real TileService
    const tilesService = jasmine.createSpyObj('TilesService', ['getTileDefinition']);

    TestBed.configureTestingModule({
      declarations: [ HostComponent, TileComponent ],
      providers: [{ provide: TilesService, useValue: tilesServiceSpy}],
      imports: [RouterTestingModule.withRoutes([])]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HostComponent);
    component = fixture.componentInstance;
    tilesServiceSpy = TestBed.get(tilesServiceSpy); // we want to get the 'real' instance from our env
  });

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

  it('should set percentcomplete on init', () => {
    expect(component.tileComponent.percentComplete).toBeUndefined('starts undefined');
    fixture.detectChanges(); // ngOnInit
    expect(component.tileComponent.percentComplete).toEqual('60%', 'set percentCompete on init')
  });
});

Upvotes: 1

Views: 944

Answers (1)

Wesley Trantham
Wesley Trantham

Reputation: 1264

In this case your line for tilesServiceSpy = TestBed.get(tilesServiceSpy); needs to instead be tilesServiceSpy = TestBed.get(TilesService);

After that change you'll likely be able to continue.

Upvotes: 2

Related Questions