How to test form.valueChanges in Angular?
Asked Answered
F

2

5

How to correctly unit test (Karma, Jasmine), that emmisions of valueChanges dispatches a FormUpdated action?

beforeEach(async(() => {
  TestBed.configureTestingModule({
    imports: [...],
    providers: [
      { provide: Store, useValue: MOCK_STORE },
    ],
    declarations: [FormComponent],
    schemas: [NO_ERRORS_SCHEMA]
  })
    .compileComponents();
}));

beforeEach(() => {
    fixture = TestBed.createComponent(FormComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
});
export class FormComponent implements OnInit {
    searchForm: FormGroup;

    constructor(private readonly fb: FormBuilder, private readonly store: Store<AppState>) {
    }

    ngOnInit(): void {
        this.searchForm = this.fb.group({});
        this.searchForm.valueChanges.subscribe(value => this.store.dispatch(new FormUpdated(value)));
    }
}

I have tried something like this:

it('should dispatch action for valueChanges', () => {
    const spy = spyOn(component['store'], 'dispatch');
    spyOn(component.searchForm, 'valueChanges').and.returnValue(of({}));

    expect(spy).toHaveBeenCalledTimes(1);
});

But this does not work - the spy has not been called.

[Edit1] - Based on comments and answer:

The problem is with the test asynchronicity. Some part of ngOnInit calls setTimeout(() => this.searchForm.get('field').updateValueAndValidity();)) which causes an emmision to this.searchForm.valueChanges() so the this.store.dispatch is actually called but after the expect(spy).toHaveBeenCalledTimes(1).

I have tried to add fakeAsync(), tick() and flushMicrotasks() but with same outcome.

it('should dispatch action for valueChanges', () => {
    const spy = spyOn(component['store'], 'dispatch');
    spyOn(component.searchForm, 'valueChanges').and.returnValue(of({}));

    tick();
    flushMicrotasks();

    expect(spy).toHaveBeenCalledTimes(1);
});
Fortify answered 17/10, 2018 at 14:7 Comment(7)
What is your beforeEach method ?Viscose
@Viscose beforeEach addedFortify
But where are you firing changes? Maybe add component.searchForm.setValue({}) before spyOn?Neopythagoreanism
@yurzui, bozkor: please see [Edit1] to clarify my issue. And sorry that I missed to add such an important piece of information.Fortify
Maybe you can manually can the ngInit during the test. it('should dispatch action for valueChanges', () => { component.ngOnInit(); }Viscose
ngOnInit is run (automatically) with the first .detectChanges which is located in beforeEach()Fortify
@Fortify good to know...Viscose
V
6

You want to test changes on a form without inputs. Maybe try with this :

this.searchForm = this.fb.group({description: ['your_input']});

.

beforeEach(async(() => {
  TestBed.configureTestingModule({
    imports: [...],
    providers: [
      { provide: Store, useValue: MOCK_STORE },
    ],
    declarations: [FormComponent],
    schemas: [NO_ERRORS_SCHEMA]
  })
    .compileComponents();
}));

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

it('should dispatch action for valueChanges', () => {
    const spy = spyOn(TestBed.get(Store<AppState>), 'dispatch') 
    component.searchForm.controls['your_input'].setValue('test') // This will trigger change
    expect(spy).toHaveBeenCalledTimes(1);
});
Viscose answered 18/10, 2018 at 9:3 Comment(1)
component.searchForm.controls['your_input'].setValue('test') this has successfully triggered the valueChange. Thank you!Succotash
R
0

I have encountered this same issue. The problem is that valueChanges cannot be spied on directly, b/c "Argument of type 'string' is not assignable to parameter of type 'never'".

I solved this by adding a pipe, and then spying on the pipe with a mock return value. So for your case, add a pipe before valueChanges in your ngOnInit():

this.searchForm.valueChanges
.pipe(takeUntil(this._destroyed))
.subscribe(value => this.store.dispatch(new FormUpdated(value)));

and then in your unit test, spy on the pipe:

spyOn(component.searchForm.valueChanges, 'pipe').and.returnValue(of({}));
Raptorial answered 14/8, 2024 at 15:53 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.