Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using [(ngModel)] does not apply change in unit test for inputs inside ngFor

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?

like image 517
Rich McCluskey Avatar asked Mar 05 '23 22:03

Rich McCluskey


1 Answers

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.

like image 140
codequiet Avatar answered Apr 24 '23 23:04

codequiet