I have recently started working with Angular testing, I made a component using Material Angular auto Complete and I have conditional rendering inside it.
I am using Karma and Jasmine for unit testing. This is the following output.
Chrome Headless 103.0.5060.114 (Windows 10): Executed 5 of 5 SUCCESS (0.219 secs / 0.186 secs)
TOTAL: 5 SUCCESS
=============================== Coverage summary ===============================
Statements : 100% ( 20/20 )
Branches : 100% ( 1/1 )
Functions : 100% ( 9/9 )
Lines : 100% ( 19/19 )
================================================================================
When I did the testing, it says the testing is 100%, So one would only assume everything is tested.
Html file
<mat-form-field
[ngStyle]="ngStyle"
appearance="outline"
class="primary-autoSelect"
floatLabel="always"
>
<mat-label>{{ label }}</mat-label>
<input
type="text"
[placeholder]="placeholder"
matInput
[formControl]="autoSelectFormControl"
[matAutocomplete]="auto"
(blur)="
onBlur();
blurExtension(
autoSelectFormControl.value,
autoSelectFormControl.valid && autoSelectFormControl.touched
)
"
/>
<mat-autocomplete #auto="matAutocomplete">
<mat-option
id="options"
*ngFor="let option of filteredOptions"
[value]="option.key"
>
{{ option.key }}
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="autoSelectFormControl.hasError('required')">
{{ label }} is required
</mat-error>
<mat-error
*ngIf="
autoSelectFormControl.hasError('validAutoCompleteOption') &&
!autoSelectFormControl.hasError('required')
"
>
Please select a value from the list {{ label }}
</mat-error>
</mat-form-field>
TS file
interface Options {
key: string;
value: string;
}
@Component({
selector: 'app-primary-auto-complete',
templateUrl: './primary-auto-complete.component.html',
styleUrls: ['./primary-auto-complete.component.css'],
})
export class PrimaryAutoCompleteComponent implements OnInit, OnChanges {
@Input() label: string = '';
@Input() placeholder: string = 'Start typing here';
@Input() options: Options[] = [];
@Input() ngStyle: { [Klass: string]: any } | null = null;
@Input() blurExtension: (value: string, isValid: boolean) => void = () => {};
@Input() autoSelectFormControl: FormControl = new FormControl('');
filteredOptions: Options[] = [];
// @ts-ignore
@ViewChild('auto') autocomplete: MatAutocomplete;
constructor() {}
// When options are updated after the API call, triggers itself to update the filtered options
ngOnChanges(changes: SimpleChanges): void {
this.filteredOptions = this._filter(this.autoSelectFormControl.value);
}
ngOnInit() {
this.autoSelectFormControl.valueChanges.subscribe((value) => {
this.filteredOptions = this._filter(value);
});
}
// Clears the option if it is a wrong one
onBlur = (): void => {
this.options.some((item) => {
if (
item.key.toLowerCase() ===
this.autoSelectFormControl.value.toLowerCase()
)
this.autoSelectFormControl.setValue(item.key);
});
};
private _filter(value: string): {
key: string;
value: string;
}[] {
const filterValue = value.toLowerCase();
return this.options.filter((item) =>
item.key.toLowerCase().startsWith(filterValue)
);
}
}
Test file
import { Component, SimpleChange, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule, ValidationErrors } from '@angular/forms';
import {
MatAutocomplete,
MatAutocompleteModule,
} from '@angular/material/autocomplete';
import { MatOption } from '@angular/material/core';
import { MatError, MatFormField, MatLabel } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { states } from 'src/app/constants/states';
import { PrimaryAutoCompleteComponent } from './primary-auto-complete.component';
describe('PrimaryAutoCompleteComponent', () => {
let component: PrimaryAutoCompleteComponent;
let fixture: ComponentFixture<PrimaryAutoCompleteComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
PrimaryAutoCompleteComponent,
MatAutocomplete,
MatFormField,
MatLabel,
MatOption,
MatError,
],
imports: [
ReactiveFormsModule,
MatInputModule,
BrowserAnimationsModule,
MatAutocompleteModule,
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PrimaryAutoCompleteComponent);
component = fixture.componentInstance;
component.ngOnInit();
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe(`When value 'Pu' is inserted into the input`, () => {
it('should filter the options and remaining options should be 2', async () => {
const stateList: {
key: string;
value: string;
}[] = states.map((states) => {
return {
key: states.value,
value: states.value,
};
});
component.options = stateList;
fixture.detectChanges();
const inputElement = fixture.debugElement.query(By.css('input'));
inputElement.nativeElement.dispatchEvent(new Event('focusin'));
inputElement.nativeElement.value = 'pu';
inputElement.nativeElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const matOptions = document.querySelectorAll('mat-option');
expect(matOptions.length).toBe(
2,
'Expect to have less options after input text and filter'
);
});
});
describe(`When value 'Pu' is inserted into the input and Punjab is clicked`, () => {
it('should filter the options and remaining options should be 2', async () => {
const stateList: {
key: string;
value: string;
}[] = states.map((states) => {
return {
key: states.value,
value: states.value,
};
});
component.options = stateList;
fixture.detectChanges();
const inputElement = fixture.debugElement.query(By.css('input'));
inputElement.nativeElement.dispatchEvent(new Event('focusin'));
inputElement.nativeElement.value = 'pu';
inputElement.nativeElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const matOptions = document.querySelectorAll('mat-option');
expect(matOptions.length).toBe(
2,
'Expect to have less options after input text and filter'
);
const optionToClick = matOptions[1] as HTMLElement;
optionToClick.click();
// Testing onChange call
component.ngOnChanges({
options: new SimpleChange(null, [], true),
});
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
// With this expect statement we verify both, proper type of event and value in it being emitted
expect(inputElement.nativeElement.value).toBe('Punjab');
});
});
describe(`When value 'punjab' is inserted into the input and on Blur is called`, () => {
it(`should change the input value to 'Punjab'`, async () => {
const stateList: {
key: string;
value: string;
}[] = states.map((states) => {
return {
key: states.value,
value: states.value,
};
});
component.options = stateList;
fixture.detectChanges();
const inputElement = fixture.debugElement.query(By.css('input'));
inputElement.nativeElement.dispatchEvent(new Event('focusin'));
inputElement.nativeElement.value = 'punjab';
inputElement.nativeElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable().then(() => {
inputElement.nativeElement.dispatchEvent(new Event('blur'));
});
fixture.detectChanges();
await fixture.whenStable();
// With this expect statement we verify both, proper type of event and value in it being emitted
expect(inputElement.nativeElement.value).toBe('Punjab');
});
});
describe(`When notjing is inserted into the input and blur event is triggered`, () => {
it('should have the required error inside the form control and render mat-error', async () => {
const stateList: {
key: string;
value: string;
}[] = states.map((states) => {
return {
key: states.value,
value: states.value,
};
});
component.options = stateList;
fixture.detectChanges();
const inputElement = fixture.debugElement.query(By.css('input'));
inputElement.nativeElement.dispatchEvent(new Event('focusin'));
// inputElement.nativeElement.value = 'pu';
// inputElement.nativeElement.dispatchEvent(new Event('input'));
// should not exist
const errorElement = fixture.debugElement.query(By.css('mat-error'));
expect(errorElement).toBeNull();
component.autoSelectFormControl.setErrors({
required: true,
} as ValidationErrors);
component.autoSelectFormControl.markAsTouched();
component.autoSelectFormControl.markAsDirty();
fixture.detectChanges();
await fixture.whenStable().then(() => {
inputElement.nativeElement.dispatchEvent(new Event('blur'));
});
fixture.detectChanges();
await fixture.whenStable().then(() => {
expect(errorElement).toBeNull();
});
fixture.detectChanges();
expect(component.autoSelectFormControl.hasError('required')).toBe(true);
});
});
});
But is it really 100% tested? I have not tested the case where error comes and it renders mat-error. Any advice is appreciated.