14
votes

I am new to Angular, I am trying to build a text field with autocomplete using Angular 5.

I found this example in Angular Material docs:

https://stackblitz.com/angular/kopqvokeddbq?file=app%2Fautocomplete-overview-example.ts

I was wondering how to write a unit test for testing the autocomplete functionality. I am setting a value to the input element and triggering an 'input' event and tried selecting the mat-option elements, but see that none of them got created:

Relevant part of my component html:

<form>
  <mat-form-field class="input-with-icon">
    <div>
      <i ngClass="jf jf-search jf-lg md-primary icon"></i>
      <input #nameInput matInput class="input-field-with-icon" placeholder="Type name here"
             type="search" [matAutocomplete]="auto" [formControl]="userFormControl" [value]="inputField">
    </div>
  </mat-form-field>
</form>

<mat-autocomplete #auto="matAutocomplete">
  <mat-option *ngFor="let option of filteredOptions | async" [value]="option.name"
              (onSelectionChange)="onNameSelect(option)">
    {{ option.name }}
  </mat-option>
</mat-autocomplete>

Spec file:

it('should filter users based on input', fakeAsync(() => {
    const hostElement = fixture.nativeElement;

    sendInput('john').then(() => {
        fixture.detectChanges();
        expect(fixture.nativeElement.querySelectorAll('mat-option').length).toBe(1);

        expect(hostElement.textContent).toContain('John Rambo');
    });
}));
function sendInput(text: string) {
    let inputElement: HTMLInputElement;

    inputElement = fixture.nativeElement.querySelector('input');
    inputElement.focus();
    inputElement.value = text;
    inputElement.dispatchEvent(new Event('input'));
    fixture.detectChanges();
    return fixture.whenStable();
}

Component html:

userFormControl: FormControl = new FormControl();

ngOnInit() {
    this.filteredOptions = this.userFormControl.valueChanges
        .pipe(
            startWith(''),
            map(val => this.filter(val))
        );
}

filter(val: string): User[] {
    if (val.length >= 3) {
        console.log(' in filter');
        return this.users.filter(user =>
            user.name.toLowerCase().includes(val.toLowerCase()));
    }
}

Before this, I realised that for making the FormControl object set the value, I have to do a inputElement.focus() first, this is something to do with using mat input of angular material. Is there something I have to do to trigger opening the mat-options pane?

How do I make this test work?

4

4 Answers

10
votes

@Adam's comment to previous answer led me to the mat-autocomplete component's own test, specially here. Where you can see that focusin is the event that opens the "options".

But they actually open in an overlay outside your component, so in my test fixture.nativeElement.querySelectorAll('mat-option').length was 0 but if i query over the element document.querySelectorAll('mat-option') I got the expected number of options.

To sumarize:

    fixture.detectChanges();
    const inputElement = fixture.debugElement.query(By.css('input')); // Returns DebugElement
    inputElement.nativeElement.dispatchEvent(new Event('focusin'));
    inputElement.nativeElement.value = text;
    inputElement.nativeElement.dispatchEvent(new Event('input'));

    fixture.detectChanges();
    await fixture.whenStable();
    fixture.detectChanges();

    const matOptions = document.querySelectorAll('mat-option');
    expect(matOptions.length).toBe(3,
      'Expect to have less options after input text and filter');

Extra ball: And if you want to click on an option (I did) you can continue like that:

    const optionToClick = matOptions[0] as HTMLElement;
    optionToClick.click();
    fixture.detectChanges();

Although I didn't success on clicking and getting the value into the input. 🤨 Well, I'm not an expert tester, but probably that behaviour should be cover in the own mat-autocomplete's tests (and actually it is) and rely on it?

8
votes

You need to add more events. I had more or less the same problem as you and it only worked when I triggered the focusin event.

I am using these events in my code. Not sure if all are needed.

inputElement.dispatchEvent(new Event('focus'));
inputElement.dispatchEvent(new Event('focusin'));
inputElement.dispatchEvent(new Event('input'));
inputElement.dispatchEvent(new Event('keydown'));

You need to add this to your sendInput function...

0
votes

I am building further upon @David answer here.

Providing component which is being tested has @Output() selectedTimezone = new EventEmitter<string>();, and in component template <mat-autocomplete #auto="matAutocomplete" (optionSelected)="selectTimezone($event.option.value)">, the unit test to capture that appropriate type of event with correct value was emitted is as follows

it('should emit selectedTimezone event on optionSelected', async() => { 
    // Note: 'selectedTimezone' is @Output event type as specified in component's signature
    spyOn(component.selectedTimezone, 'emit'); 

    const inputElement = fixture.debugElement.query(By.css('input'));
    inputElement.nativeElement.dispatchEvent(new Event('focusin'));

    /**
     * Note, mat-options in this case set up to have array of ['Africa/Accra (UTC
     * +01:00)', 'Africa/Addis_Ababa (UTC +01:00)', 'Africa/Algiers (UTC +01:00)',
     * 'Africa/Asmara (UTC +01:00)']. I am setting it up in 'beforeEach'
     */
    inputElement.nativeElement.value = 'Africa'; 
    inputElement.nativeElement.dispatchEvent(new Event('input'));

    await fixture.whenStable();

    const matOptions = document.querySelectorAll('mat-option');
    expect(matOptions.length).toBe(4);

    const optionToClick = matOptions[0] as HTMLElement;
    optionToClick.click();

    // With this expect statement we verify both, proper type of event and value in it being emitted
    expect(component.selectedTimezone.emit).toHaveBeenCalledWith('Africa/Accra');
  });
0
votes

Thanks to @David's answer I got everything working until selecting an option part.

To make the option selection work, I had to do this;

...
matOptions[0].dispatchEvent(new Event('click'));
...

And you don't have to cast the type of matOptions[0] to HTMLElement

Note** I am on Angular 8.0.1 and solution could be different in the new version