Home > database >  Testing an Angular Form with Jest - fill and query form fields
Testing an Angular Form with Jest - fill and query form fields

Time:12-28

How can I query and fill form fields in a Jest test script? My working environment is Angular 13.

In saw similar posts and was inspired in using alternative answers. But I keep on getting errors that properties don't exist.

My simplified form is:

<form name="frmProduct">
  <div >
    <label  for="ProductPrice">Product price</label>
    <input  id="ProductPrice" name="ProductPrice" [(ngModel)]="productPrice" type="text">
  </div>
  <div >
    <input  (click)="clear()" type="button" value="Clear">
    <input  (click)="save()" type="button" value="Save">
  </div>
</form>

Try-1: My Jest test code is ... having errors that the property does not exist.

describe('ProductFormComponent', () => {
  let component: ProductcomponentComponent;
  let fixture: ComponentFixture<ProductcomponentComponent>;
  let button: HTMLElement;

  beforeAll(() => {
    TestBed.resetTestEnvironment();
    TestBed.initTestEnvironment(BrowserDynamicTestingModule,
      platformBrowserDynamicTesting());
  });
  beforeEach( () => {
    TestBed.configureTestingModule({
      declarations: [ProductcomponentComponent],
      imports: [FormsModule, ReactiveFormsModule ]
    }).compileComponents(); 
  });

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

  // Below is the synchronous way. I also tried the async way via fakeAsync( .. )
  it('should calculate tax based on base price when save button is clicked', () => {
    productPrice = 4000;
    fixture.detectChanges();
    expect( getFormElementValue( fixture.nativeElement, '#ProductPrice')).toEqual( '4000')
  });
  
  function getFormElementValue( componentForm: any, selector:string): string {
    return componentForm.querySelector( selector).value;
  }
});

EDIT: with the help of '@rfprod' solved it. Thank you very much!

Set via component and read via form field:

it('should calculate tax based on base price when save button is clicked', async () => {
  component.productPrice = 4000;
  fixture.detectChanges();
  fixture.whenStable().then( () => {
    expect(getFormElementValue(fixture.nativeElement, '#ProductPrice')).toEqual('4000')
  });
});

function getFormElementValue( componentForm: any, selector:string): string {
  return componentForm.querySelector( selector).value;
}

Setting and reading form fields:

it('should calculate tax based on base price when save button is clicked', async () => {
  component.product.BasePrice = 4000;
  fixture.detectChanges();
  fixture.whenStable().then( () => {
    setFormElementValue(fixture.nativeElement, '#BasePrice', 5000)
  });
  fixture.whenStable().then( () => {
    expect(getFormElementValue(fixture.nativeElement, '#BasePrice')).toEqual('5000')
  });
});

function setFormElementValue(componentForm: any, selector: string, value: number) {
  let element = componentForm.querySelector( selector);
  element.value = value;
  element.dispatchEvent(new Event('input'));
  fixture.detectChanges();
}

CodePudding user response:

seems like it should be

it('should calculate tax based on base price when save button is clicked', () => {
    component.productPrice = 4000;
    fixture.detectChanges();
    expect( getFormElementValue( fixture.nativeElement, '#ProductPrice')).toEqual( '4000')
  });

I'd suggest using the reactive forms instead of [(ngModel)] enter image description here


Edit: here's an updated, sync version

So, just for the sake of completeness, here's an updated version for you, with ReactiveFormsModule. Reactive Forms are synchronous and while they add a bit of boilerplate in one place, you save it in many others.

So my template would have

<input [formControl]="productPriceControl">

instead of

<input [(ngModel)]="productPrice">

My component would have:

class FormComponent {
  productPriceControl = new FormControl('');
  ...
  // here's how to read the value from the control
  save() {
    const currentPrice = this.productPriceControl.value;
    // calculate price with tax
  }
  // here's how to write the value
  clear() {
    this.productPriceControl.setValue('');
  }
  ...
}

Now, my test looks like this:

import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { FormComponent } from './form.component';
import { EventEmitter, Output } from '@angular/core';

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

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

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

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should set the price in the form, when we set it in the model', () => {
    component.productPriceControl.setValue('4000');
    expect(fixture.nativeElement.querySelector('#ProductPrice').value).toEqual('4000');
  });
  it('should set the price in the control, when we set it in the template', () => {
    const input = fixture.nativeElement.querySelector('#ProductPrice');
    input.value = '4000';
    input.dispatchEvent(new Event('input'));
    expect(component.productPriceControl.value).toEqual('4000')
  });
});

As you can see, there is no asynchronous code, no waitForAsync or anything. Everything is synced right away.

Hope that helps for some future work.

  • Related