3

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.

user2146414
  • 840
  • 11
  • 29
  • I would actually take a step back and ask what the purpose of this test is. It seems like you are testing Angular more than you are testing your own logic. At best you are testing whether or not validation on this form was setup correctly. – joshrathke Apr 04 '20 at 20:39
  • @joshrathke I don't agree with you. It's just a simple example but what I want to test is: 1) I want to see that username that I believe that is valid does not cause form validation error 2) I want to see that invalid (in a certain way) username results in an appropriate error message (here I have `.error.username` but for missing value I could have something like `.error.username.required`) – user2146414 Apr 04 '20 at 22:21
  • It would be helpful to see the code you're testing. A minimal reproducible example. – x00 Apr 18 '20 at 21:09
  • Seconded for the code you’re testing and perhaps a bit more of the actual test code. How you’re building the fixture and whatnot. – bryan60 Apr 18 '20 at 21:28
  • Can you please add HTML code as well? A stackblitz would be awesome. Please provide detailed code as another part of code might be the reason for your issue. – Jasdeep Singh Apr 19 '20 at 07:07
  • What is the connection between `myForm` and `registerForm`. If it's the same form then your code works for me. Please create reproducible example in stackblitz – yurzui Apr 19 '20 at 10:08
  • @user2146414 https://stackoverflow.com/a/61403830/1277159 – WSD Apr 26 '20 at 10:20

3 Answers3

3

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
      });
    });
  });
});
spots
  • 2,483
  • 5
  • 23
  • 38
1

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.

WSD
  • 3,243
  • 26
  • 38
0

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()
    
Jasdeep Singh
  • 7,901
  • 1
  • 11
  • 28