angulartestingjasminekarma-jasmineangular-reactive-forms

Jasmine test: Expected spy updateValueAndValidity to have been called


I have an Angular component with this method:

deleteRule(index: number): void {
    this.rules = this.formGroup.get('rules') as FormArray;

    if (this.rules.length > 1) {
        this.rules.controls.splice(index, 1);
        this.rules.controls.forEach(control => control.updateValueAndValidity());
    }
}

And a corresponding test:

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


it('should update validity of remaining controls after deleting a rule', () => {
    let rules: FormArray = component.formGroup.get('rules') as FormArray;
    rules.push(component.createRule());
    rules.push(component.createRule());
    component.rules = rules;
    const control1 = component.rules.at(0);
    const control2 = component.rules.at(1);
    spyOn(control1, 'updateValueAndValidity').and.callThrough();
    spyOn(control2, 'updateValueAndValidity').and.callThrough();

    component.deleteRule(0);

    expect(control1.updateValueAndValidity).toHaveBeenCalled();
    expect(control2.updateValueAndValidity).toHaveBeenCalled();
});

Even though I can see that a breakpoint in

this.rules.controls.forEach(control => control.updateValueAndValidity());

is hit, I still get an error:

Expected spy updateValueAndValidity to have been called.

Can anyone help me understand why? And hopefully come up with a fix. :-)


Solution

  • You are removing one of the controls in the function and then running the for loop, hence, for the control that was removed, the function updateValueAndValidity is never called, you can change your code to check only for the controls that are present.

    it('should update validity of remaining controls after deleting a rule', () => {
      let rules: FormArray = component.formGroup.get('rules') as FormArray;
      rules.push(component.createRule());
      rules.push(component.createRule());
      component.rules = rules;
      const control1 = component.rules.at(0);
      const control2 = component.rules.at(1);
      spyOn(control1, 'updateValueAndValidity').and.callThrough();
      spyOn(control2, 'updateValueAndValidity').and.callThrough();
    
      component.deleteRule(0);
    
      // expect(control1.updateValueAndValidity).toHaveBeenCalled();
      expect(control2.updateValueAndValidity).toHaveBeenCalled();
    });
    

    Stackblitz Demo


    When you call the method updateValueAndValidity at the form array level, it will call the same method at the child level, so you can also change the code to below.

    ts:

    deleteRule(index: number): void {
      this.rules = this.formGroup.get('rules') as FormArray;
    
      if (this.rules.length > 1) {
        this.rules.controls.splice(index, 1);
        this.rules.updateValueAndValidity();
      }
    }
    

    test:

    it('should update validity of remaining controls after deleting a rule', () => {
      let rules: FormArray = component.formGroup.get('rules') as FormArray;
      rules.push(component.createRule());
      rules.push(component.createRule());
      component.rules = rules;
      spyOn(rules, 'updateValueAndValidity').and.callThrough();
      component.deleteRule(0);
      expect(rules.updateValueAndValidity).toHaveBeenCalled();
    });
    

    Stackblitz Demo