使用 Jasmine 测试时如何模拟 MatChipInput

How to mock MatChipInput when testing with Jasmine

我已经设置了一个 stackblitz 来基本显示问题所在。

基本上,当我尝试在包含 MatChipList 的 MatFormField 上触发事件时,我收到

错误
 Cannot read property 'stateChanges' of undefined at MatChipInput._onInput

我已经尝试用 MatInput 的替代模拟覆盖 MatChip 模块。我也试过重写指令。

HTML

 <h1>Welcome to app!!</h1>

 <div>
  <mat-form-field>
   <mat-chip-list #chipList>
    <mat-chip *ngFor="let contrib of contributors; let idx=index;" [removable]="removable" (removed)="removeContributor(idx)">
     {{contrib.fullName}}
    </mat-chip>
    <input  id="contributor-input"
        placeholder="contributor-input"
        #contributorInput
        [formControl]="contributorCtrl"
        [matAutocomplete]="auto"
        [matChipInputFor]="chipList"
        [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
        [matChipInputAddOnBlur]="addOnBlur">
  </mat-chip-list>
 </mat-form-field>
</div>

TS

import { Component, Input } from '@angular/core';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

contributors = [{fullName: 'foo bar'}];
removable = true;
addOnBlur = false;
separatorKeysCodes: number[] = [
    ENTER,
    COMMA,
];

contributorCtrl = new FormControl();

filteredPeople: Observable<Array<any>>;

@Input() peopleArr = [];

constructor() {
   this.filteredPeople = this.contributorCtrl.valueChanges.pipe(startWith(''), map((value: any) => 
this.searchPeople(value)));
 }

 searchPeople(searchString: string) {
    const filterValue = String(searchString).toLowerCase();
    const result = this.peopleArr.filter((option) => option.fullName.toLowerCase().includes(filterValue));
    return result;
  }
}

规格

import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { 
  BrowserDynamicTestingModule, 
  platformBrowserDynamicTesting 
} from '@angular/platform-browser-dynamic/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {  MatFormFieldModule, 
      MatAutocompleteModule, 
      MatInputModule,
      MatChipsModule } from '@angular/material';
import { FormsModule, ReactiveFormsModule} from '@angular/forms';

describe('AppComponent', () => {

  const mockPeopleArray = [
    { personId: 1,
      email: 'foo1@bar.com',
      department: 'fake1',
      username: 'foo1',
      fullName: 'Foo Johnson'
     },
     { personId: 2,
      email: 'foo2@bar.com',
      department: 'fake1',
      username: 'foo2',
      fullName: 'John Fooson'
     },
     { personId: 3,
      email: 'foo1@bar.com',
      department: 'fake2',
      username: 'foo3',
      fullName: 'Mary Smith'
     }
 ];


 let app: AppComponent;
 let fixture: ComponentFixture<AppComponent>;
 let nativeElement: HTMLElement;

 beforeAll( ()=> {
  TestBed.initTestEnvironment(BrowserDynamicTestingModule, 
  platformBrowserDynamicTesting());
  });
  beforeEach(
   async(() => {
     TestBed.configureTestingModule({
       imports: [
       RouterTestingModule,
       MatFormFieldModule,
       FormsModule,
       ReactiveFormsModule,
       MatAutocompleteModule,
       MatChipsModule,
       MatInputModule,
       NoopAnimationsModule
       ],
       declarations: [AppComponent]
     }).compileComponents();

   fixture = TestBed.createComponent(AppComponent);
   app = fixture.debugElement.componentInstance;
   nativeElement = fixture.nativeElement;
  })
 );
 it(
 'should render title \'Welcome to app!!\' in a h1 tag', async(() => {
  fixture.detectChanges();
  expect(nativeElement.querySelector('h1').textContent).toContain('Welcome to app!!');
})
);

it('searchPeople should trigger and filter', (done) => {
  app.peopleArr = mockPeopleArray;

  const expected = [
    { personId: 3,
      email: 'foo1@bar.com',
      department: 'fake2',
      username: 'foo3',
      fullName: 'Mary Smith'
     }
  ];

  const myInput = <HTMLInputElement> 
  nativeElement.querySelector('#contributor-input');
  expect(myInput).not.toBeNull();
  myInput.value = 'Mar';
  spyOn(app, 'searchPeople').and.callThrough();
  myInput.dispatchEvent(new Event('input'));
    fixture.detectChanges();
    fixture.whenStable().then(() => {
        const myDiv = nativeElement.querySelector('#contrib-div');
        expect(app.searchPeople).toHaveBeenCalledWith('mar');
        app.filteredPeople.subscribe(result => 
        expect(result).toEqual(<any>expected));
        done();
    });
  });
 });

您将获得:

Cannot read property 'stateChanges' of undefined at MatChipInput._onInput

因为 Angular 在触发时尚未完成绑定myInput.dispatchEvent(new Event('input'))

要解决此问题,您应该先调用 fixture.detectChanges,以便 Angular 执行数据绑定。

那么您不需要将此测试设为异步,因为所有操作都是同步执行的。

现在关于您的 searchPeople 方法。自从您使用 startWith(''):

以初始值开始订阅后,它将被调用两次
this.contributorCtrl.valueChanges.pipe(startWith('')

因此您需要跳过第一次调用并在触发 input 事件后检查调用结果。

app.filteredPeople.pipe(skip(1)).subscribe(result => {
  ...
});

spyOn(app, "searchPeople").and.callThrough();

myInput.dispatchEvent(new Event("input"));
expect(app.searchPeople).toHaveBeenCalledWith("Mar");

测试的全部代码:

it("searchPeople should trigger and filter", () => {
  app.peopleArr = mockPeopleArray;

  const expected = [
    {
      personId: 3,
      email: "foo1@bar.com",
      department: "fake2",
      username: "foo3",
      fullName: "Mary Smith"
    }
  ];

  fixture.detectChanges();
  const myInput = nativeElement.querySelector<HTMLInputElement>(
    "#contributor-input"
  );
  expect(myInput).not.toBeNull();
  myInput.value = "Mar";

  app.filteredPeople.pipe(skip(1)).subscribe(result => 
    expect(result).toEqual(expected);
  );

  spyOn(app, "searchPeople").and.callThrough();

  myInput.dispatchEvent(new Event("input"));
  expect(app.searchPeople).toHaveBeenCalledWith("Mar");
}); 

Forked Stackblitz