Angular - test displayed error on form validation

Viewed 982

I have a test that looks like this:

...
component.registerForm.controls['username'].setValue('VALID USERNAME');

fixture.detectChanges();

expect(component.registerForm.valid).toEqual(true);  // FIRST ASSERTION
const errors = fixture.debugElement.queryAll(By.css('.error.username'));
expect(errors.length).toBe(0);  // <-- SECOND ASSERTION: IT VAILS HERE!!!

The problem is that even if form is valid and the first assertion passes the second assertion fails because "required" error message is displayed. To me it looks as if the form were updated while fixture.debugElement not so that it displays the initial error. I.e. it ignores setting username value.

UPDATED: I have very similar problem and believe that the source might be the same: I have some test cases I would like to make sure that my form validation is set up correctly (i.e. valid cases result in valid form, invalid cases in invalid form) (I simplified the form to username only):

getUsernameCases().forEach((testCase) => {
  it(`should be valid: ${testCase.valid}`, () => {
    component.myForm.get('username').setValue(testCase.username);

    component.registerForm.updateValueAndValidity();
    fixture.detectChanges();
    component.registerForm.updateValueAndValidity(); // Second time to make sure order does not matter

    expect(component.myForm.valid).toBe(testCase.valid); // ALWAYS INVALID HERE
  });
});

The problem is that the form is always invalid regardless of value. It has required error - it means that is shows form initial state. Before setValue.

3 Answers

This will largely depend on how you are building your form, but I can write a test that asserts the form validity in the way I think you're trying. I'd need to see the component under test to know for sure.

Here is a stackblitz showing these tests.

app.component.ts:

export class AppComponent implements OnInit {

  myForm: FormGroup;

  //Helper to get the username control to check error state
  get username() {
    return this.myForm.get('username');
  }

  constructor(private fb: FormBuilder) {
  }

  ngOnInit() {
    //"Username" has two validations to test: required and minLength
    this.myForm = this.fb.group({
      'username': new FormControl(null, [Validators.required, Validators.minLength(10)])
    });
  }
}

app.component.html:

<form [formGroup]="myForm">
  <!-- "username" input with conditional styles based on error state -->
  <input type="text" 
         formControlName="username"
         [class.username-valid]="!username.errors"
         [class.error-username-required]="username.errors?.required"
         [class.error-username-minlength]="username.errors?.minlength">
</form>

app.component.spec.ts:

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let component: AppComponent;

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

  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  describe('The form validity', () => {
    const testCases = [
      {
        expected: true,
        username: 'VALID_USERNAME',
        expectedCss: 'username-valid'
      },
      {
        expected: false,
        username: null,
        expectedCss: 'error-username-required'
      },
      {
        expected: false,
        username: 'too_short',
        expectedCss: 'error-username-minlength'
      }
    ];

    testCases.forEach(testCase => {
      it(`should update the form validity: ${testCase.expected}`, () => {
        component.myForm.get('username').setValue(testCase.username);
        component.myForm.updateValueAndValidity();
        fixture.detectChanges();
        expect(component.myForm.valid).toBe(testCase.expected); //assert the form is valid/invalid
        expect(fixture.debugElement.query(By.css(`.${testCase.expectedCss}`))).toBeTruthy(); //assert the conditional style is applied
      });
    });
  });
});

I was facing a similar issue a few days ago. I solved it by calling

component.myForm.get('username').updateValueAndValidity({ emitEvent: true })

Instead of component.myForm.updateValueAndValidity();

Basically you need to trigger the recalculation of the value and the validation at the FormControl level, rather than in the FormGroup level.

Probably you will not need the emitEvent parameter, in my case I have a subscription on the FormGroup to know whether the status of the form was valid or not, by providing {emitEvent: true} Angular will emit statusChanges & valueChanges and the subscription will do their job.

I requested you to provide the HTML code for your issue. Programmatically changing the value does not mark the dirty property as true or false (i am assuming this to be the cause of your issue). You can resolve it using multiple ways:

  1. I would suggest you change the value of DOM elements instead of changing it programmatically.

    let inputEl = fixture.debugElement.queryAll(By.css('.inputElClass'));
    let inputElement = inputEl.nativeElement;
    
        //set input value
    inputElement.value = 'test value';
    inputElement.dispatchEvent(new Event('input'));
    
    fixture.detectChanges();
    
    expect(component.registerForm.valid).toEqual(true);  // FIRST ASSERTION
    const errors = fixture.debugElement.queryAll(By.css('.error.username'));
    expect(errors.length).toBe(0);  
    
  2. You need to use the FormControl API methods (markAsDirty, markAsDirty) like below.

    const formcontrol = component.registerForm.controls['username']
    formcontrol.setValue('VALID USERNAME');
    this.formName.markAsDirty()
    this.formName.markAsTouched()
    
Related