J-man
J-man

Reputation: 1833

Why is change event for reactive form control not firing in Jasmine Angular unit test?

I have a change function I am passing to the change event for a reactive form control that evaluates the dirty state and to check if there are any errors on the control and if so then set a boolean flag to true/false. This boolean flag is then used to determine whether or not to show a <div> element that has an error message. This works just fine in the browser, but when the unit test runs, the "dirty" is never being set to true. Here is my code:

HTML

<form [formGroup]="myForm" novalidate>
    <input id="age" formControlName="age" (change)="onChange()" />
    <div id="ageError" *ngIf="ageIsError()">
        <label>Age has errored</label>
    </div>
</form>

Component

constructor(private fb: FormBuilder) {}

ngOnInit() {
    this.myForm = this.fb.group({
        age: [null, [Validators.min(18)]]
    });
}

onChange() {
    if (this.ageIsError())
        // do something
}

ageIsError() {
    return this.myForm.controls.age.hasError('min') &&
           this.myForm.controls.age.dirty;
}

Unit Test

it('should show error message when age is less than 18', fakeAsync(() => {
    let age = component.myForm.controls.age;
    age.setValue('10', { emitEvent: true });
    fixture.detectChanges();

    fixture.whenStable().then(() => {
        let ageError = debugElement.query(By.css('#ageError')).nativeElement;
        expect(component.ageIsError()).toBe(true);
        expect(ageError.innerText).toContain('Age has errored');
    });
}));

Again, the actual implementation works in the browser, but the unit test fails. Does anyone know hoe to emit the event in jasmine to set the control to a dirty state, or is there a better way to achieve this? Thanks!

Upvotes: 8

Views: 6914

Answers (2)

JoshuaTree
JoshuaTree

Reputation: 1261

The form control bindings are updated automatically.

The below code is within a test block. It simply gets an input field and dispatches an update value on it.

I then render out the controller state and can confirm the bindings worked automatically.

  const usernameInput: HTMLInputElement = fixture.nativeElement.querySelector(
            '#username'
        );
        usernameInput.value = 'testValueNotEmail';
        usernameInput.dispatchEvent(new Event('input'));

        // FormGroup and Controll Bindings and Validations were automatically updated after the event.
        console.log(component.loginForm.getRawValue());
        console.log(component.loginForm.get('username')?.errors);
        console.log(component.loginForm.get('username')?.touched);
        console.log(component.loginForm.get('username')?.dirty);

        // In order for the fixture render element changes to take effect,
        // I had to detect the changes. The form error fields were then rendered. 
        fixture.detectChanges();
        const errorField = fixture.nativeElement.querySelector(
            '#usernameSmall'
        );
        console.log(errorField);

Upvotes: 0

shokha
shokha

Reputation: 3179

In your example age.setValue(...) actually sets correct value to the input, but it doesn't append ageError to the DOM - BECAUSE there wasn't real/emulated event to mark the control as dirty. This is why the method ageIsError always returns false in this case.

As a workaround I just emulated input event using document.createEvent('Event') and seems like it works fine:

  it('should show error message when age is less than 18', async(() => {
    const customEvent: Event = document.createEvent('Event');
    customEvent.initEvent('input', false, false);
    const ageInput = fixture.debugElement.query(By.css('input#age')).nativeElement;
    ageInput.value = 10;
    ageInput.dispatchEvent(customEvent);

    fixture.detectChanges();

    fixture.whenStable().then(() => {
      let ageError = fixture.debugElement.query(By.css('#ageError')).nativeElement;
      expect(component.ageIsError()).toBe(true);
      expect(ageError.innerText).toContain('Age has errored');
    });
  }));

I also found the fix to your solution - just call age.markAsDirty() before detectChanges:

  it('should show error message when age is less than 18', async(() => {
    let age = component.myForm.controls.age;
    age.setValue('10'); // { emitEvent: true } is by default
    age.markAsDirty(); // add this line

    fixture.detectChanges();

    fixture.whenStable().then(() => {
        let ageError = fixture.debugElement.query(By.css('#ageError')).nativeElement;
        expect(component.ageIsError()).toBe(true);
        expect(ageError.innerText).toContain('Age has errored');
    });
  }));

I've also created a stackblitz example, please check it out as well. Hope these solutions will be helpful for you :)

Upvotes: 2

Related Questions