diff --git a/README.md b/README.md index c219308..8d55b8d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ All the scenarios are listed here below and nicely linked to the source file. _Learn how to mock out nested components which you don't want to necessarily test_ * [testing component with service deps](./src/app/components/component-mock-external.component.spec.ts) _Simple test of component logic by manually instantiating the component_ + * [testing component with template-driven form](./src/app/components/template-driven-forms.component.spec.ts) + _Simple test of component with template driven form_ + * [testing component with reactive (model-driven) form](./src/app/components/reactive-forms.compnent.spec.ts) + _Simple test of component with reactive form_ 1. [**Testing Services**](./src/app/services) * [Simple stateless function](./src/app/services/greeting.service.spec.ts) _Learn about different ways of injecting a service into a test case as well as how to test service methods._ diff --git a/src/app/components/reactive-forms.compnent.spec.ts b/src/app/components/reactive-forms.compnent.spec.ts new file mode 100644 index 0000000..8b6f380 --- /dev/null +++ b/src/app/components/reactive-forms.compnent.spec.ts @@ -0,0 +1,140 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; +import { ReactiveFormsComponent, User } from "./reactive-forms.component"; + + +describe('Component: ReactiveForms', () => { + + let component: ReactiveFormsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + + // refine the test module by declaring the test component + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, FormsModule], + declarations: [ReactiveFormsComponent] + }); + + // create component and test fixture + fixture = TestBed.createComponent(ReactiveFormsComponent); + + // get test component from the fixture + component = fixture.componentInstance; + component.ngOnInit(); + }); + + it('form invalid when empty', () => { + expect(component.form.valid).toBeFalsy(); + }); + + it('name field validity', () => { + let errors = {}; + let name = component.form.get('name'); + expect(name.valid).toBeFalsy(); + + errors = name.errors || {}; + expect(errors['required']).toBeTruthy(); + + name.setValue("Jo"); + errors = name.errors || {}; + expect(errors['required']).toBeFalsy(); + expect(errors['minlength']).toBeTruthy(); + + name.setValue("John qwerty asdfgh zxcvbnm Doe"); + errors = name.errors || {}; + expect(errors['required']).toBeFalsy(); + expect(errors['minlength']).toBeFalsy(); + expect(errors['maxlength']).toBeTruthy(); + + name.setValue("John Doe"); + errors = name.errors || {}; + expect(errors['required']).toBeFalsy(); + expect(errors['minlength']).toBeFalsy(); + expect(errors['maxlength']).toBeFalsy(); + }); + + it('email field validity', () => { + let errors = {}; + let email = component.form.get('email'); + expect(email.valid).toBeFalsy(); + + errors = email.errors || {}; + expect(errors['required']).toBeTruthy(); + + email.setValue("test"); + errors = email.errors || {}; + expect(errors['required']).toBeFalsy(); + expect(errors['pattern']).toBeTruthy(); + + email.setValue("test@example.com"); + errors = email.errors || {}; + expect(errors['required']).toBeFalsy(); + expect(errors['pattern']).toBeFalsy(); + }); + + it('password field validity', () => { + let errors = {}; + let password = component.form.get('password'); + + errors = password.errors || {}; + expect(errors['required']).toBeTruthy(); + + password.setValue("Qwerty"); + errors = password.errors || {}; + expect(errors['required']).toBeFalsy(); + expect(errors['minlength']).toBeTruthy(); + + password.setValue("Qwertyui9!"); + errors = password.errors || {}; + expect(errors['required']).toBeFalsy(); + expect(errors['minlength']).toBeFalsy(); + }); + + it('confirm password field validity', () => { + let passwordErrors = {}; + let confirmPasswordErrors = {}; + let password = component.form.get('password'); + let confirmPassword = component.form.get('confirmPassword'); + + password.setValue("Qwertyui9!"); + passwordErrors = password.errors || {}; + confirmPasswordErrors = confirmPassword.errors || {}; + expect(passwordErrors['required']).toBeFalsy(); + expect(confirmPasswordErrors['required']).toBeTruthy(); + + password.setValue("Qwertyui9!"); + confirmPassword.setValue("Qwertyui"); + confirmPasswordErrors = confirmPassword.errors || {}; + expect(confirmPasswordErrors['required']).toBeFalsy(); + expect(confirmPasswordErrors['passwordDoesntMatch']).toBeTruthy(); + + password.setValue("Qwertyui9!"); + confirmPassword.setValue("Qwertyui9!"); + confirmPasswordErrors = confirmPassword.errors || {}; + expect(confirmPasswordErrors['required']).toBeFalsy(); + expect(confirmPasswordErrors['passwordDoesntMatch']).toBeFalsy(); + + }); + + it('submitting a form emits a user', () => { + expect(component.form.valid).toBeFalsy(); + component.form.get('name').setValue("John Doe"); + component.form.get('email').setValue("xyz@test.com"); + component.form.get('password').setValue("Qwertyui9!"); + component.form.get('confirmPassword').setValue("Qwertyui9!"); + expect(component.form.valid).toBeTruthy(); + + let user: User; + component.register.subscribe((value) => user = value); + + // Trigger the signup function + component.signup(); + + // Testing the emitted value is correct + expect(user.name).toBe("John Doe"); + expect(user.email).toBe("xyz@test.com"); + expect(user.password).toBe("Qwertyui9!"); + expect(user.confirmPassword).toBe("Qwertyui9!"); + }); +}); \ No newline at end of file diff --git a/src/app/components/reactive-forms.component.ts b/src/app/components/reactive-forms.component.ts new file mode 100644 index 0000000..ba093d0 --- /dev/null +++ b/src/app/components/reactive-forms.component.ts @@ -0,0 +1,70 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormGroup, Validators, FormBuilder, FormControl } from "@angular/forms"; + +export class User { + constructor(public name: string, public email: string, + public password: string, public confirmPassword: string) { + } +} + +@Component({ + selector: 'app-signup', + template: ` +
+ + + + + + + + + +
+ ` +}) +export class ReactiveFormsComponent { + @Output() register = new EventEmitter(); + form: FormGroup; + + constructor(private formBuilder: FormBuilder) { } + + ngOnInit() { + this.form = this.formBuilder.group({ + name: ['', [ + Validators.required, + Validators.minLength(3), + Validators.maxLength(20)]], + email: ['', [ + Validators.required, + Validators.pattern("[^ @]*@[^ @]*")]], + password: ['', [ + Validators.required, + Validators.minLength(8)]], + confirmPassword: ['', [ + Validators.required, + this.checkConfirmPassword.bind(this)]], + }); + } + + checkConfirmPassword(control: FormControl):{[s: string]: boolean} { + if(this.form && this.form.value.password !== control.value) { + return { 'passwordDoesntMatch':true }; + } + return null; + } + + signup() { + console.log(`Signup` + JSON.stringify(this.form.value)); + if (this.form.valid) { + this.register.emit( + new User( + this.form.value.name, + this.form.value.email, + this.form.value.password, + this.form.value.confirmPassword + ) + ); + } + } +} \ No newline at end of file diff --git a/src/app/components/template-driven-forms.component.spec.ts b/src/app/components/template-driven-forms.component.spec.ts new file mode 100644 index 0000000..8dd4850 --- /dev/null +++ b/src/app/components/template-driven-forms.component.spec.ts @@ -0,0 +1,79 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { FormsModule } from "@angular/forms"; +import { TemplateDrivenFormsComponent, UserLogin } from "./template-driven-forms.component"; + + +describe('Component: TemplateDrivenFormsComponent', () => { + + let component: TemplateDrivenFormsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + + // refine the test module by declaring the test component + TestBed.configureTestingModule({ + imports: [FormsModule], + declarations: [TemplateDrivenFormsComponent] + }); + + fixture = TestBed.createComponent(TemplateDrivenFormsComponent); + + component = fixture.componentInstance; + + fixture.detectChanges(); + + }); + + it('form invalid when empty', () => { + // The fixture.whenStable() returns a promise that resolves when the JavaScript engine's task queue becomes empty. + fixture.whenStable().then( () => { + expect(component.form.valid).toBeFalsy(); + }); + + }); + + it('form invalid when incompletely filled', () => { + fixture.whenStable().then( () => { + component.form.controls['email'].setValue('xyz@test.com'); + fixture.detectChanges(); + fixture.whenStable().then( () => { + fixture.detectChanges(); + expect(component.form.valid).toBeFalsy(); + expect(component.form.controls.password.errors.required).toBeTruthy(); + }); + }); + + }); + + it('form valid when completely filled', () => { + fixture.whenStable().then( () => { + component.form.controls['email'].setValue('xyz@test.com'); + component.form.controls['password'].setValue('Qwertyui9!'); + fixture.detectChanges(); + fixture.whenStable().then( () => { + fixture.detectChanges(); + expect(component.form.valid).toBeTruthy(); + }); + }); + + }); + + it('form valid & expect correct logon credentials when clicked submit', () => { + fixture.whenStable().then( () => { + component.form.controls['email'].setValue('xyz@test.com'); + component.form.controls['password'].setValue('Qwertyui9!'); + fixture.detectChanges(); + fixture.whenStable().then( () => { + fixture.detectChanges(); + expect(component.form.valid).toBeTruthy(); + let user: UserLogin; + component.login.subscribe((value) => user = value); + component.logon(); + expect(user.email).toBe("xyz@test.com"); + expect(user.password).toBe("Qwertyui9!"); + }); + }); + + }); + +}); \ No newline at end of file diff --git a/src/app/components/template-driven-forms.component.ts b/src/app/components/template-driven-forms.component.ts new file mode 100644 index 0000000..70ec24b --- /dev/null +++ b/src/app/components/template-driven-forms.component.ts @@ -0,0 +1,43 @@ +import { Component, EventEmitter, Output, ViewChild } from '@angular/core'; + +export class UserLogin { + constructor(public email: string, public password: string) { + } +} + +@Component({ + selector: 'app-login', + template: ` +
+ + + + + +
+ ` +}) +export class TemplateDrivenFormsComponent { + @Output() login = new EventEmitter(); + @ViewChild('f') form: any; + model: UserLogin = { + email: '', + password: '' + }; + + constructor() { } + + ngOnInit() { } + + logon() { + console.log(`Logon` + JSON.stringify(this.model)); + if (this.form.valid) { + this.login.emit( + new UserLogin( + this.form.value.email, + this.form.value.password + ) + ); + } + } +} \ No newline at end of file