0

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.

Van Wilder
  • 444
  • 4
  • 14
  • `When I did the testing, it says the testing is 100%` Where are you taking this from? And what does "100% " mean? –  Jul 11 '22 at 09:31
  • Editing the question rn. – Van Wilder Jul 11 '22 at 12:06
  • Thanks for clarifying! The 100% simply means that all of your typescript code was executed. It does NOT mean that your html code is fully tested. It is merely a guidance for you to see how much of your typescript code was executed. –  Jul 11 '22 at 12:14
  • I am planning to run a CI/CD pipeline that fails if a certain percentage of coverage is not there. This seems to be a huge loophole. Any idea how I can include HTML as well. I worked with jest and react before, they included HTML as well. – Van Wilder Jul 11 '22 at 12:24

0 Answers0