5

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.

1 Answer 1

2

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!

Sign up to request clarification or add additional context in comments.

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.