Search code examples
angulartypescriptjasminekarma-jasminekarma-runner

Failed: Cannot read property 'setValue' of undefined for a login test


I have written the following tests:

import { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AuthenticationService } from '../services/authentication.service';
import { By } from '@angular/platform-browser';
import { LoginComponent } from './login.component';
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterTestingModule } from '@angular/router/testing';
import { GlobalDataService } from '../services/global-data.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { DatePipe } from '@angular/common';
import { MatDialogModule } from '@angular/material/dialog';
import { OverlayModule } from '@angular/cdk/overlay';


describe('LoginComponent', () => {
  let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [ LoginComponent ],
      imports: [ ReactiveFormsModule, MatFormFieldModule, MatProgressSpinnerModule, RouterTestingModule, HttpClientTestingModule,
         OverlayModule, MatDialogModule ],
      providers: [ GlobalDataService, DatePipe, AuthenticationService ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should set instance correctly', () => {
    expect(component).not.toBeNull();
   });

  it('should call auth login method', waitForAsync(() => {
    let loginElement: DebugElement;
    const debugElement = fixture.debugElement;
    const authenticationService = debugElement.injector.get(AuthenticationService);
    const loginSpy = spyOn(authenticationService, 'login').and.callThrough();
    loginElement = fixture.debugElement.query(By.css('form'));
    // to set values
    component.loginForm.controls.username.setValue('[email protected]');
    component.loginForm.controls.password.setValue('testpassword');
    loginElement.triggerEventHandler('ngSubmit', null);
    expect(loginSpy).toHaveBeenCalledTimes(1); // check that service is called once
    // ^ this fails without correct login!
   }));

  it('should render "Login" at the top of the login page', () => {
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h5').textContent).toContain('Login');
  });
});

My Login.component.ts:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

import { AuthenticationService } from '../services/authentication.service';
import { GlobalDataService } from '../services/global-data.service';

/* tslint:disable variable-name */
@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  returnUrl: string;
  loginForm: FormGroup;
  submitted = false;
  error = '';
  loading = false;
  public errorMsg = 'Please login to continue';
  public redirected: boolean;
  public utm_source: string;

  constructor(private router: Router, private formBuilder: FormBuilder, public DataService: GlobalDataService,
              public authenticationService: AuthenticationService, private activatedRoute: ActivatedRoute) {
      this.activatedRoute.queryParams.subscribe(params => {
      const param = params.utm_source;

      if ([ 'a', 'b', 'c', 'd', 'e'].includes(param)) {
        this.redirected = true;
        this.utm_source = param;
      } else {
        this.redirected = false;
      }
  });
  }

  ngOnInit(): void {
    this.loginForm = this.formBuilder.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(6)]]
  });
  }

// convenience getter for easy access to form fields
get f() { return this.loginForm.controls; }

  onSubmit(loginsubmit) {
    this.submitted = true;
    // stop here if form is invalid
    if (this.loginForm.invalid) {
        return console.log('LoginForm Invalid');
    }
    this.loading = true;

    this.authenticationService.login(this.f.email.value, this.f.password.value)
        .pipe()
        .subscribe(
            data => {
              // it was this.returnUrl instead of avior/dashboard
                if (this.redirected) {
                  this.router.navigate([this.utm_source]);
                } else {
                  this.router.navigate(['landing-page']);
                }
            },
            error => {
                console.log('Login->authservice->err: ', error);
                this.error = error;
                this.loading = false;
            });
}
}

The error I get happens on the should call auth login method test with the following error:

Failed: Cannot read property 'setValue' of undefined

So it appears that the LoginForm form is somewhy and somehow undefined. Any clue why that might be? The definition of the form field appears to be fine, I have honestly no clue what's wrong. The most weird part is that it used to work and now it doesn't after migrating the code. I tried mendling with the code with no avail.


Solution

  • In you test setup you are trying to set a value on a control that doesn't exist in your form.

    component.loginForm.controls.username.setValue('[email protected]'); // username doesn't exisit

    the property is called email. try to change the line to:

    component.loginForm.controls.email.setValue('[email protected]');