Search code examples
angulartypescriptangular2-changedetection

angular change detection in interaction test


I have a simple reactive form (angular 11)

<form class="searchForm" [formGroup]="form">
  <input formControlName="firstName" required/>
  <button [disabled]="! form.valid">Submit</button>
</form>
@Component({
  selector: 'app-simple-form',
  templateUrl: './my-form.component.html',
  styleUrls: ['./my-form.component.css']
})
export class MyFormComponent {

  form: FormGroup;

  constructor() {
    this.form = new FormGroup({
      firstName: new FormControl()
    });
  }
}

I am trying to do a simple interaction test, I am setting a value in the required text field, the triggering an input event. I am expecting that after the event angular runs change detection and would update the disabled state of the button, but this does not happen. I have to trigger change detection manually to have the button have the correct disabled state. I am slightly puzzled about this behaviour.

 it('should enable submit button', () => {

    const input: HTMLInputElement = document.querySelector('input[formControlName="firstName"]');
    const button: HTMLButtonElement = document.querySelector('button');

    // simulate user interaction
    input.focus();
    input.value = 'john';

    expect(component.form.valid).toBeFalse();
    expect(button.disabled).toBeTruthy();

    const event = new Event('input', {
      bubbles: true,
      cancelable: true,
    });
    input.dispatchEvent(event);

    // required field has text -> form valid
    expect(component.form.valid).toBeTrue();

    // but button is still disabled
    expect(button.disabled).toBeTrue();

    fixture.detectChanges();

    // only after detectChanges the disabled state is correct
    expect(button.disabled).toBeFalse();

  });


Solution

  • By default, you have to manually call change detection in Angular spec tests. This is explained in the Angular docs "Component testing scenarios".

    In production, change detection kicks in automatically when Angular creates a component or the user enters a keystroke or an asynchronous activity (e.g., AJAX) completes.

    Delayed change detection [in tests] is intentional and useful. It gives the tester an opportunity to inspect and change the state of the component before Angular initiates data binding and calls lifecycle hooks.

    You can enable automatic change detection in Angular spec tests. This can be useful if you don't want to deal with manually calling change detection (either because you need to call it a lot or don't feel it's useful for your scenario). It's possible to enable automatic change detection by configuring the TestBed with the ComponentFixtureAutoDetect provider.

    i.e.,

    TestBed.configureTestingModule({
      declarations: [ MyFormComponent ],
      providers: [
        { provide: ComponentFixtureAutoDetect, useValue: true }
      ]
    });
    

    However, be aware there are some limits to using this provider.

    The ComponentFixtureAutoDetect service responds to asynchronous activities such as promise resolution, timers, and DOM events.

    So this should work in your case because you are performing a DOM interaction via the input.dispatchEvent() call. However, if you want to directly update a component without DOM interaction, you need to call detectChanges().

    But a direct, synchronous update of the component property is invisible. The test must call fixture.detectChanges() manually to trigger another cycle of change detection.

    i.e.,

    it('should display updated title after detectChanges', () => {
      comp.title = 'Test Title';
      fixture.detectChanges(); // detect changes explicitly to see direct component update.
      expect(h1.textContent).toContain(comp.title);
    });
    

    See Automatic change detection in Angular docs for more info.