How to change value of a select box in angular2 unit test?

Paul Becotte picture Paul Becotte · Sep 30, 2016 · Viewed 24.5k times · Source

I have an Angular2 component that contains a select box that looks like

<select [(ngModel)]="envFilter" class="form-control" name="envSelector" (ngModelChange)="onChangeFilter($event)">
    <option *ngFor="let env of envs" [ngValue]="env">{{env}}</option>
</select>

I am trying to write a unit test for the ngModelChange event. This is my latest failing attempt

it("should filter and show correct items", async(() => {
    fixture.detectChanges();
    fixture.whenStable().then(() => {
        el = fixture.debugElement.query(By.name("envSelector"));
        fixture.detectChanges();
        makeResponse([hist2, longhist]);
        comp.envFilter = 'env3';
        el.triggerEventHandler('change', {});
        fixture.whenStable().then(() => {
            fixture.detectChanges();
            expect(comp.displayedHistory).toEqual(longhist);
        });
    });

The part I am having trouble with is that changing the value of the underlying model comp.envFilter = 'env3'; does not trigger the change method. I added el.triggerEventHandler('change', {}); but this throws Failed: Uncaught (in promise): ReferenceError: By is not defined. I cannot find any hints in the documentation... any ideas?

Answer

Paul Samsotha picture Paul Samsotha · Sep 30, 2016

As far as the error. It seems like you just need to import By. This is not something that is global. It should be imported from the following module

import { By } from '@angular/platform-browser';

As far as the testing part, this is what I have been able to figure out. When you change a value in a the component, you need to trigger a change detection to update the view. You do this with fixture.detectChanges(). Once this is done, normally the view should be updated with the value.

From testing something similar to your example, it seems this is not the case though. It seems there is still some asynchronous task going on after the change detection. Say we have the following

const comp = fixture.componentInstance;
const select = fixture.debugElement.query(By.css('select'));

comp.selectedValue = 'a value';
fixture.DetectChanges();
expect(select.nativeElement.value).toEqual('1: a value');

This doesn't seem to work. It appears there is some async going on causing the value not to be set yet. So we need to wait for the async tasks by calling fixture.whenStable

comp.selectedValue = 'a value';
fixture.DetectChanges();
fixture.whenStable().then(() => {
  expect(select.nativeElement.value).toEqual('1: a value');
});

The above would work. But now we need to trigger the change event as that doesn't happen automatically.

fixture.whenStable().then(() => {
  expect(select.nativeElement.value).toEqual('1: a value');

  dispatchEvent(select.nativeElement, 'change');
  fixture.detectChanges();
  fixture.whenStable().then(() => {
    // component expectations here
  });
});

Now we have another asynchronous task from the event. So we need to stabilize it again

Below is a complete test that I tested with. It's a refactor of the example from the source code integration tests. They used fakeAsync and tick which is similar to using async and whenStable. But with fakeAsync, you can't use templateUrl, so I though it would be best to refactor it to use async.

Also the source code tests does kind of a double one way testing, first testing model to view, then view to model. While it looks like your test was trying to do kind of a two-way test, from model around back to model. So I refactored it a bit to suite your example better.

import { Component } from '@angular/core';
import { TestBed, getTestBed, async } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { dispatchEvent } from '@angular/platform-browser/testing/browser_util';

@Component({
  selector: 'ng-model-select-form',
  template: `
    <select [(ngModel)]="selectedCity" (ngModelChange)="onSelected($event)">
      <option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
    </select>
  `
})
class NgModelSelectForm {
  selectedCity: {[k: string]: string} = {};
  cities: any[] = [];

  onSelected(value) {
  }
}

describe('component: NgModelSelectForm', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ FormsModule ],
      declarations: [ NgModelSelectForm ]
    });
  });

  it('should go from model to change event', async(() => {
    const fixture = TestBed.createComponent(NgModelSelectForm);
    const comp = fixture.componentInstance;
    spyOn(comp, 'onSelected');
    comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
    comp.selectedCity = comp.cities[1];
    fixture.detectChanges();
    const select = fixture.debugElement.query(By.css('select'));

    fixture.whenStable().then(() => {
      dispatchEvent(select.nativeElement, 'change');
      fixture.detectChanges();
      fixture.whenStable().then(() => {
        expect(comp.onSelected).toHaveBeenCalledWith({name : 'NYC'});
        console.log('after expect NYC');
      });
    });
  }));
});