Я создал 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);
}
}