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();
});
});
});
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With