Угловые испытания проваливаются случайным образом - PullRequest
0 голосов
/ 02 марта 2019

Я создал RegisterComponent с формой.Для аутентификации я использую Firebase Auth.Как и каждый второй или третий раз, когда я запускаю свой тест, я получаю ошибку «Uncaught TypeError: Невозможно прочитать свойство« user »с неопределенной брошенной ошибкой».Если я закомментирую проваленный тест, я получу ту же ошибку к другому тесту позже.Это совершенно случайно.Если я закомментирую тело формы onSubmit, я не получу ошибок, так что я предполагаю, что проблема есть, но я не могу понять это.

Мое репо: https://github.com/GBRBD/salty/tree/feature/auth

Я думаю, что проблема в этом тесте:

 it('should submit form when form is valid ', () => {
      username.setValue('xxxx');
      email.setValue('xxxx@xxx.com');
      password.setValue('xx627JHxxxx');

      spyOn(authService, 'signUp').and.returnValue(Promise.resolve());

      component.onSubmit();
      fixture.detectChanges();
      expect(authService.signUp).toHaveBeenCalled();
    });

Внутри метода onSubmit он обнаруживает, что результат не определен.Как мне проверить этот код?

register.component.html

<mat-card>
  <mat-card-header>
    <mat-card-title>Register</mat-card-title>
  </mat-card-header>

  <mat-card-content>
    <form
      (ngSubmit)="onSubmit()"
      [formGroup]="registerForm"
      #formDirective="ngForm"
    >
      <mat-form-field hintLabel="Max 12 characters">
        <input
          matInput
          placeholder="Username"
          formControlName="username"
          required
          maxlength="12"
        />

        <mat-hint align="end"
          >{{ registerForm.controls.username.value?.length || 0 }}/12</mat-hint
        >
        <mat-error
          class="username"
          *ngIf="
            registerForm.controls.username.touched &&
            registerForm.controls.username.hasError('required')
          "
          >{{ errorMessages.emptyUsernameError }}</mat-error
        >
        <mat-error
          class="username"
          *ngIf="
            registerForm.controls.username.touched &&
            registerForm.controls.username.hasError('maxlength')
          "
          >{{ errorMessages.tooLongUsernameError }}</mat-error
        >
      </mat-form-field>
      <mat-form-field>
        <input
          matInput
          placeholder="E-mail address"
          formControlName="email"
          required
          type="email"
        />

        <mat-error
          class="email"
          *ngIf="
            registerForm.controls.email.touched &&
            registerForm.controls.email.hasError('required')
          "
          >{{ errorMessages.emptyEmailError }}</mat-error
        >
      </mat-form-field>
      <mat-form-field hintLabel="Minimum 6, maximum 32 characters">
        <input
          matInput
          placeholder="Password"
          formControlName="password"
          required
          type="password"
          maxlength="32"
        />
        <mat-hint align="end"
          >{{ registerForm.controls.password.value?.length || 0 }}/32</mat-hint
        >
        <mat-error
          class="password"
          *ngIf="
            registerForm.controls.password.touched &&
            registerForm.controls.password.hasError('required')
          "
          >{{ errorMessages.emptyPasswordError }}</mat-error
        >
        <mat-error
          class="password"
          *ngIf="
            registerForm.controls.password.touched &&
            registerForm.controls.password.hasError('maxlength')
          "
          >{{ errorMessages.tooLongPasswordError }}</mat-error
        >
      </mat-form-field>
      <button
        type="submit"
        mat-raised-button
        color="primary"
        [disabled]="!registerForm.valid"
      >
        Register
      </button>
    </form>
  </mat-card-content>
  <mat-card-actions>
    <p>Already have an account? <a routerLink="/login">Log in!</a></p>
  </mat-card-actions>
</mat-card>

register.component.spec.ts

    import { async, ComponentFixture, TestBed } from '@angular/core/testing';
    import { RegisterComponent } from './register.component';
    import { SharedModule } from 'src/app/shared/shared.module';
    import { ReactiveFormsModule, AbstractControl } from '@angular/forms';
    import { RouterTestingModule } from '@angular/router/testing';
    import { AuthService } from 'src/app/shared/services/auth.service';
    import { environment } from 'src/environments/environment';
    import { AngularFireModule } from '@angular/fire';
    import { AngularFireAuth } from '@angular/fire/auth';
    import { HelperService } from 'src/app/shared/services/helper.service';

    describe('RegisterComponent', () => {
      let component: RegisterComponent;
      let fixture: ComponentFixture<RegisterComponent>;
      let registerElement: HTMLElement;
      let username: AbstractControl;
      let email: AbstractControl;
      let password: AbstractControl;
      let authService: AuthService;
      let helperService: HelperService;
      let errors;

      beforeEach(async(() => {
        TestBed.configureTestingModule({
          imports: [
            SharedModule,
            ReactiveFormsModule,
            RouterTestingModule,
            AngularFireModule.initializeApp(environment.firebase)
          ],
          declarations: [RegisterComponent],
          providers: [AngularFireAuth, AuthService, HelperService]
        }).compileComponents();
      }));

      beforeEach(() => {
        fixture = TestBed.createComponent(RegisterComponent);
        component = fixture.componentInstance;
        helperService = TestBed.get(HelperService);
        authService = TestBed.get(AuthService);
        fixture.detectChanges();
        errors = {};
        registerElement = fixture.nativeElement;
        username = component.registerForm.controls.username;
        email = component.registerForm.controls.email;
        password = component.registerForm.controls.password;
      });

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

      it(`should have a title with 'Register' `, () => {
        const title = registerElement.querySelector('mat-card-title');
        expect(title.textContent).toContain('Register');
      });

      describe('Register Form', () => {
        it('should be invalid when empty', () => {
          expect(component.registerForm.valid).toBeFalsy();
        });

        it(`should have a button with text 'Register'`, () => {
          const submitButton = registerElement.querySelector('button');
          expect(submitButton.textContent).toContain('Register');
        });

        it('Submit button should be disabled when form is invalid', () => {
          const submitButton = registerElement.querySelector('button');
          expect(submitButton.disabled).toBeTruthy();
        });

        it('should submit form when form is valid ', () => {
          username.setValue('xxxx');
          email.setValue('xxxx@xxx.com');
          password.setValue('xx627JHxxxx');

          spyOn(authService, 'signUp').and.returnValue(Promise.resolve());

          component.onSubmit();
          fixture.detectChanges();
          expect(authService.signUp).toHaveBeenCalled();
        });

        it('should have disabled submit button when form is invalid', () => {
          const submitButton = registerElement.querySelector('button');
          expect(submitButton.disabled).toBeTruthy();
        });

        it(`should have a button with text 'register'`, () => {
          const submitButton = registerElement.querySelector('button');
          expect(submitButton.textContent).toContain('Register');
        });

        describe('Username Field', () => {
          it('should create username field with empty string', () => {
            expect(username.value).toBeFalsy();
          });

          it('should be invalid when empty', () => {
            errors = username.errors;
            expect(errors.required).toBeTruthy();
            expect(username.valid).toBeFalsy();
          });

          it('should be invalid when input length is less than 4 character', () => {
            username.setValue('xx');
            expect(username.valid).toBeFalsy();
          });

          it('should be valid when input length is greater than or equal to 4 character', () => {
            username.setValue('xxxx');
            expect(username.valid).toBeTruthy();
          });

          it('should be invalid when input length is greater than 12 character', () => {
            const longString = helperService.longStringMaker(13);
            username.setValue(longString);
            expect(username.valid).toBeFalsy();
          });

          it('should show error messages when input is empty', () => {
            username.setValue('');
            username.markAsTouched();
            fixture.detectChanges();
            const errorMessage = registerElement.querySelector(
              'mat-error.username'
            );
            expect(errorMessage.textContent).toContain(
              component.errorMessages.emptyUsernameError
            );
          });

          it('should show error messages when input too long', () => {
            const longString = helperService.longStringMaker(13);
            username.setValue(longString);
            username.markAsTouched();
            fixture.detectChanges();
            const errorMessage = registerElement.querySelector(
              'mat-error.username'
            );
            expect(errorMessage.textContent).toContain(
              component.errorMessages.tooLongUsernameError
            );
          });
        });

        describe('Email Field', () => {
          it('should create email field with empty string', () => {
            expect(email.value).toBeFalsy();
          });

          it('should be invalid when empty', () => {
            errors = email.errors;
            expect(errors.required).toBeTruthy();
            expect(email.valid).toBeFalsy();
          });

          it('should show error messages when input is empty', () => {
            email.setValue('');
            email.markAsTouched();
            fixture.detectChanges();
            const errorMessage = registerElement.querySelector('mat-error.email');
            expect(errorMessage.textContent).toContain(
              component.errorMessages.emptyEmailError
            );
          });
        });

        describe('Password Field', () => {
          it('should create password field with empty string', () => {
            expect(password.value).toBeFalsy();
          });

          it('should be invalid when empty', () => {
            errors = password.errors;
            expect(errors.required).toBeTruthy();
            expect(password.valid).toBeFalsy();
          });

          it('should be invalid when input length is less than 6 character', () => {
            password.setValue('xx');
            expect(password.valid).toBeFalsy();
          });

          it('should be valid when input length is greater than or equal to 6 character', () => {
            password.setValue('xxxxxx');
            expect(password.valid).toBeTruthy();
          });

          it('should be invalid when input length is greater than 32 character', () => {
            const longString = helperService.longStringMaker(33);
            password.setValue(longString);
            expect(password.valid).toBeFalsy();
          });

          it('should show error messages when input is empty', () => {
            password.setValue('');
            password.markAsTouched();
            fixture.detectChanges();
            const errorMessage = registerElement.querySelector(
              'mat-error.password'
            );
            expect(errorMessage.textContent).toContain(
              component.errorMessages.emptyPasswordError
            );
          });

          it('should show error messages when input too long', () => {
            const longString = helperService.longStringMaker(33);
            password.setValue(longString);
            password.markAsTouched();
            fixture.detectChanges();
            const errorMessage = registerElement.querySelector(
              'mat-error.password'
            );
            expect(errorMessage.textContent).toContain(
              component.errorMessages.tooLongPasswordError
            );
          });
        });
      });
    });

register.component.ts

import { Component, OnInit, ViewChild, NgZone } from '@angular/core';
import {
  FormGroupDirective,
  FormGroup,
  FormBuilder,
  Validators
} from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from 'src/app/shared/services/auth.service';
import { User } from 'src/app/shared/models/user.model';
import { from } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { UserService } from 'src/app/shared/services/user.service';
@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {
  @ViewChild('formDirective') formDirective: FormGroupDirective;
  registerForm: FormGroup;
  errorMessages = {
    emptyUsernameError: 'Please enter a username!',
    tooLongUsernameError: 'Username is too long! Max 12 character!',
    emptyEmailError: 'Please enter an E-mail address!',
    emptyPasswordError: 'Please enter a password!',
    tooLongPasswordError: 'Password is too long! Max 32 characters!'
  };
  constructor(
    private fb: FormBuilder,
    private router: Router,
    public authService: AuthService,
    public ngZone: NgZone,
    public userService: UserService
  ) {}
  ngOnInit() {
    this.initializeForm();
  }
  onSubmit() {
    const user: User = {
      username: this.registerForm.value.username,
      email: this.registerForm.value.email,
      password: this.registerForm.value.password
    };
    this.ngZone.run(() => {
      if (this.registerForm.valid) {
        from(this.authService.signUp(user))
          .pipe(
            map(result => {
              return result.user.uid;
            }),
            switchMap(uid => {
              user.uid = uid;
              return this.userService.saveUser(user);
            })
          )
          .subscribe(() => {
            this.router.navigate(['/']);
          });
      }
    });
  }
  private initializeForm() {
    this.registerForm = this.fb.group({
      username: this.initUsernameField(),
      email: this.initEmailField(),
      password: this.initPasswordField()
    });
  }
  private initUsernameField(): any {
    return [
      null,
      [Validators.required, Validators.minLength(4), Validators.maxLength(12)]
    ];
  }
  private initEmailField(): any {
    return [null, [Validators.required, Validators.email]];
  }
  private initPasswordField(): any {
    return [
      null,
      [Validators.required, Validators.minLength(6), Validators.maxLength(32)]
    ];
  }
}

auth.service.ts

import * as Firebase from 'firebase';
import { Observable, from } from 'rxjs';
import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { tap, switchMap, map, take } from 'rxjs/operators';
import { User } from '../models/user.model';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  user: Observable<User>;

  constructor(private afAuth: AngularFireAuth) {
    this.getUserStateFromFirebase();
  }

  get getUserState(): Observable<User> {
    return this.user;
  }

  get getIdToken(): Observable<string | null> {
    return this.afAuth.idToken;
  }

  signIn(user: User): Promise<firebase.auth.UserCredential> {
    return this.afAuth.auth.signInWithEmailAndPassword(
      user.email,
      user.password
    );
  }

  signUp(user: User): Promise<firebase.auth.UserCredential> {
    return this.afAuth.auth.createUserWithEmailAndPassword(
      user.email,
      user.password
    );
  }

  signOut(): Promise<void> {
    return this.afAuth.auth.signOut();
  }

  updateEmail(newEmail: string) {
    return this.afAuth.auth.currentUser.updateEmail(newEmail);
  }

  updatePassword(newPassword: string) {
    return this.afAuth.auth.currentUser.updatePassword(newPassword);
  }

  sendPasswordReset(email: string) {
    return this.afAuth.auth.sendPasswordResetEmail(email);
  }

  verifyPasswordResetCode(oobCode: string) {
    return this.afAuth.auth.verifyPasswordResetCode(oobCode);
  }

  confirmPasswordReset(oobCode: string, newPassword: string) {
    return this.afAuth.auth.confirmPasswordReset(oobCode, newPassword);
  }

  reauthenticateAndRetrieveDataWithCredential(email: string, password: string) {
    const credential = this.getEmailProviderCredential(email, password);
    return this.afAuth.auth.currentUser.reauthenticateAndRetrieveDataWithCredential(
      credential
    );
  }

  private getUserStateFromFirebase() {
    this.user = this.afAuth.user;
  }

  private getEmailProviderCredential(email: string, password: string) {
    return Firebase.auth.EmailAuthProvider.credential(email, password);
  }
}
...