使用 Jest 测试 Angular 表单 - 填写和查询表单字段

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

如何在 Jest 测试脚本中查询和填写表单字段?我的工作环境是Angular13.

In 看到了类似的帖子并受到启发使用替代答案。但我不断收到属性不存在的错误。

我的简化形式是:

<form name="frmProduct">
  <div class="form-group row">
    <label class="col-2" for="ProductPrice">Product price</label>
    <input class="col-8 form-control" id="ProductPrice" name="ProductPrice" [(ngModel)]="productPrice" type="text">
  </div>
  <div class="form-group row">
    <input class="col-2 btn btn-warning pl-1" (click)="clear()" type="button" value="Clear">
    <input class="col-2 btn btn-success pl-2" (click)="save()" type="button" value="Save">
  </div>
</form>

Try-1:我的 Jest 测试代码是...有 属性 不存在的错误。

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;
  }
});

编辑:在“@rfprod”的帮助下解决了这个问题。非常感谢!

组件设置,表单域读取:

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;
}

设置和读取表单域:

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();
}

似乎应该

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')
  });

我建议使用反应形式而不是 [(ngModel)] https://angular.io/api/forms/ReactiveFormsModule

你的测试有一些问题。第一,你的笑话测试似乎不完整。例如,您需要在 组件 属性.

上设置产品价格

第二,当您获得价格时,您需要更好地定位选择器。

这里有几个对我有用的例子:

import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule } 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: [FormsModule],
    })
    .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.productPrice = '4000';
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelector('#ProductPrice').value).toEqual('');
    fixture.whenStable()
      .then(() => {
        expect(fixture.nativeElement.querySelector('#ProductPrice').value).toEqual('4000');
      });
  });

  it('should do this with "waitForAsync"', waitForAsync(() => {
    component.productPrice = '4000';
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelector('#ProductPrice').value).toEqual('');
    fixture.whenStable()
      .then(() => {
        expect(fixture.nativeElement.querySelector('#ProductPrice').value).toEqual('4000');
      });
  }));

  // maybe you wanna create an output and listen for changes?
  // like, save will output productPrice + 20%
  // your component has:
  //     @Output() priceWithTax = new EventEmitter<string>();
  // and the save method:
  //    this.priceWithTax.emit( <product price increased by 20 %> )
  it('should do this with "waitForAsync"', waitForAsync((done: any) => {
    component.priceWithTax
      .subscribe((value: number) => {
        expect(value).toEqual(4800);
        done();
      })
    component.productPrice = '4000';
    fixture.detectChanges();
  }));
});

我的组件模板与您的相似:

<form name="frmProduct">
  <div class="form-group row">
    <label class="col-2" for="ProductPrice">Product price</label>
    <input class="col-8 form-control" id="ProductPrice" name="ProductPrice" [(ngModel)]="productPrice" type="text">
  </div>
  <div class="form-group row">
    <input class="col-2 btn btn-warning pl-1" (click)="clear()" type="button" value="Clear">
    <input class="col-2 btn btn-success pl-2" (click)="save()" type="button" value="Save">
  </div>
</form>

而我的组件class是这样的:

import { Component, OnInit, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'ng-jest-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.scss']
})
export class FormComponent implements OnInit {
  @Output() priceWithTax = new EventEmitter<number>();
  productPrice = '';
  constructor() { }

  ngOnInit(): void {
    console.log('initialized')
  }

  clear() {
    this.productPrice = '';
  }

  save() {
    console.log('PP:', this.productPrice);
    const price = parseInt(this.productPrice, 10);
    const value = Math.round(price * 1.2)
    this.priceWithTax.emit(value)
  }

}

这些是我得到的结果:


编辑:这是一个更新的同步版本

所以,为了完整起见,这里有一个更新版本,带有 ReactiveFormsModule。响应式表单 是同步的 ,虽然它们在一个地方添加了一些样板,但您可以在其他许多地方保存它。

所以我的模板会有

<input [formControl]="productPriceControl">

而不是

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

我的组件将具有:

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('');
  }
  ...
}

现在,我的测试是这样的:

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')
  });
});

如您所见,没有异步代码,没有waitForAsync或任何东西。一切都立即同步。

希望对以后的工作有所帮助。