Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock MatChipInput when testing with Jasmine

I have set-up a stackblitz with a basic showing of what the issue is.

Basically when I try to trigger an event on a MatFormField which contains a MatChipList I am getting an error of

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

I have tried overriding the MatChip module with a replacement mock for MatInput. I've also tried overriding the directive.

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

SPEC

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: '[email protected]',
      department: 'fake1',
      username: 'foo1',
      fullName: 'Foo Johnson'
     },
     { personId: 2,
      email: '[email protected]',
      department: 'fake1',
      username: 'foo2',
      fullName: 'John Fooson'
     },
     { personId: 3,
      email: '[email protected]',
      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: '[email protected]',
      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();
    });
  });
 });
like image 800
E. Maggini Avatar asked Nov 08 '19 16:11

E. Maggini


1 Answers

You're getting:

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

since Angular hasn't finished bindings yet at the time of firingmyInput.dispatchEvent(new Event('input'))

To remedy this you should call fixture.detectChanges first so that Angular will perform data binding.

Then you do not need to make this test asynchrounous since all actions are executed synchronously.

Now regarding your searchPeople method. It will be called twice since you start subscription with initial value by using startWith(''):

this.contributorCtrl.valueChanges.pipe(startWith('')

So you need to skip the first call and check the result of the call after firing input event.

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

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

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

The whole code of the test:

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

  const expected = [
    {
      personId: 3,
      email: "[email protected]",
      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

like image 66
yurzui Avatar answered Nov 11 '22 05:11

yurzui