4

I'm having trouble testing an Angular component that utilizes the two way [(ngModel)] binding on checkbox inputs inside an ngFor. It works just fine in the actual app. This is just a problem with the test.

Here's an example test that fails:

import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { Component, EventEmitter, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';

describe('Example Test', () => {
  @Component({
    template: `
      <input *ngFor="let value of values"
             type="checkbox"
             class="checkbox-1"
             [(ngModel)]="value.isSelected"
             (change)="output.emit(values)">
    `,
    styles: [``]
  })
  class TestHostComponent {
    @Output() output: EventEmitter<any> = new EventEmitter();

    values = [
      { isSelected: true },
      { isSelected: true },
      { isSelected: true },
    ];
  }

  let testHost: TestHostComponent;
  let fixture: ComponentFixture<TestHostComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [FormsModule],
      declarations: [TestHostComponent],
      providers: []
    })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestHostComponent);
    testHost = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should change isSelected', fakeAsync(() => {
    const spy = spyOn(testHost.output, 'emit');
    fixture.nativeElement.querySelectorAll('.checkbox-1')[0].click();
    fixture.detectChanges();
    tick();

    expect(spy).toHaveBeenCalledWith([
      { isSelected: false }, // it fails because this is still true
      { isSelected: true },
      { isSelected: true },
    ]);
  }));
});

Using [(ngModel)] with a single input that's not in a loop works fine in a similar test. I've even logged the emitted value from (ngModelChange) and when the checkbox is clicked $event is true when it should really be false.

Any ideas?

Rich McCluskey
  • 1,571
  • 1
  • 13
  • 24

1 Answers1

6

It seems like that method of performing a click isn't triggering change detection. Dispatching a change event on the checkbox instead gave the expected result:

it('should change isSelected', fakeAsync(() => {
    const spy = spyOn(testHost.output, 'emit');
    const checkbox = fixture.nativeElement.querySelectorAll('.checkbox-1')[0];
    checkbox.dispatchEvent(new Event('change'));
    fixture.detectChanges();
    tick();

    expect(spy).toHaveBeenCalledWith([
        { isSelected: false }, // This is now false
        { isSelected: true },
        { isSelected: true },
    ]);
}));

Solution inspired by this post.

UPDATE:

It looks like there's a need to wait for some controls to be initialized or registered on the CheckboxControlValueAccessor. If you modify the second beforeEach() to wait for one cycle after creating the component, the original test code works:

beforeEach(fakeAsync(() => {
    fixture = TestBed.createComponent(TestHostComponent);
    testHost = fixture.componentInstance;
    fixture.detectChanges();
    tick();
}));

See this Github issue for the full answer/explanation.

codequiet
  • 964
  • 3
  • 8
  • Thanks, that worked. However, it seems like there might be a bug somewhere if Angular's change detection is not triggered from click events in tests. The real behavior I want to test is that clicking the checkbox results in a change. This seems like the best option right now so I'll mark it as the solution. – Rich McCluskey Dec 10 '18 at 18:01
  • 1
    Good point, I did a little poking around and found what I believe is the right answer/explanation. See this link: https://github.com/angular/angular/issues/17104. I tried it out by adding fakeAsync and tick to the second beforeEach function and your original test worked. I'll edit my answer to include this. – codequiet Dec 11 '18 at 06:53
  • Not for nothing - I was having similar issue. Except that I was trying to change value from the other direction, programmatically and testing to see if the input had changed. Tests failed until I added fakeAsync/tick – terary Jul 01 '19 at 07:29