Search code examples
angularunit-testingangular2-formsangular2-testingangular-forms

Angular 2 - Unit test binding to nested custom form control


I've got a custom from control with selector app-date-picker. It implements ControlValueAccessor. I have a component called MyPage that contains this custom form control via:

<app-date-picker class="address__from-date" [(ngModel)]="fromDate"></app-date-picker>

I'm trying to write a unit test for MyPage that tests both directions of binding. I have done this for other form fields just fine, for example:

it('should bind zip code', fakeAsync(() => {
  const formControl = element.query(By.css('.address__zip-code input')).nativeElement;

  // model -> view
  component.zipCode = 76777;
  fixture.detectChanges();
  tick();

  expect(formControl.value).toEqual('76777');

  // view -> model
  formControl.value = '89556';
  formControl.dispatchEvent(new Event('input'));

  expect(component.zipCode).toEqual(89556);
}));

My problem arises when I try to do this for my custom form control. So far, I can only test one direction of binding, and even so it is requiring the use of ng-reflect-model, which is just awful:

it('should bind from-date', fakeAsync(() => {
  const formControl = element.query(By.css('.address__from-date app-date-picker')).nativeElement;

  // model -> view
  component.fromDate = '01/2017';
  fixture.detectChanges();
  tick();

  expect(formControl.attributes['ng-reflect-model'].value).toEqual('01/2017');

  // view -> model
  // Not sure what to do here either
}));

Is there a better way to go about doing this? I'd like to:

  1. Be able to test both directions of binding from the MyPage unit tests, so that I know I wired it up to the form controls correctly
  2. Not have to use ng-reflect-*

Other notes:

The MyPage component is a standard component with a fromDate (and a zipCode) field.

The custom form field implements ControlValueAccessor correctly, has a date field, and uses an <input> internally, which itself is bound via ngModel:

<input [(ngModel)]="date" type="date">

DatePickerComponent

@Component({
  selector: 'app-date-picker',
  templateUrl: './date-picker.component.html',
  styleUrls: ['./date-picker.component.scss'],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => DatePickerComponent),
    multi: true
  }]
})
export class DatePickerComponent implements ControlValueAccessor {

  private propagateChange = (_: any) => {};

  private _date: string;
  get date(): string {
    return this._date;
  }

  @Input()
  set date(value: string) {
    this._date = value;
    this.propagateChange(value);
  }

  writeValue(newValue: any) {
    if (newValue !== undefined) {
      this.date = newValue;
    }
  }

  registerOnChange(fn: any) {
    this.propagateChange = fn;
  }

  registerOnTouched(fn: any) {
    // Not used
  }
}

UPDATE 8/10/2017

So far, I have gotten close to what I want with @ViewChild:

@Component(...)
MyPageComponent {
 @ViewChild('fromDate') fromDatePicker: DatePickerComponent;
 ...
}

The MyPage template becomes (note the template reference variable #fromDate):

<app-date-picker #fromDate class="address__from-date" [(ngModel)]="fromDate">
</app-date-picker>

Then the test becomes:

it('should bind from-date', fakeAsync(() => {
  // model -> view
  component.info.fromDate = '01/2017';
  fixture.detectChanges();
  tick();

  expect(component.fromDatePicker.date).toEqual('01/2017');

  // view -> model
  component.fromDatePicker.date = '02/2017';

  expect(component.info.fromDate).toEqual('02/2017');
}));

Anyone know of a better way? This gets me by for now, though it's not optimal.


Solution

  • Alright, finally found an answer I'm happy with, as it avoids both ng-reflect-* and @ViewChild. Instead of calling .nativeElement, I can call .componentInstance. Then the test becomes simply:

    it('should bind from-date', fakeAsync(() => {
      const formControl = element.query(By.css('.address__from-date app-date-picker')).componentInstance;
    
      // model -> view
      component.fromDate = '01/2017';
      fixture.detectChanges();
      tick();
    
      expect(formControl.date).toEqual('01/2017');
    
      // view -> model
      formControl.date = '02/2017';
    
      expect(component.fromDate).toEqual('02/2017');
    }));
    

    I'm still open if anyone has a better solution, but I'll mark this as the answer for now. Hope it helps anyone else running into this!