Chapter 6: Testing and Optimizing Angular Applications
Activity 15: Animating the Route Transition Between the Blog Post Page and the View Post Page of the Blogging Application
Import the routing module into the app.routing.module file using the following code:
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserModule } from '@angular/platform-browser'; ……………….. imports: [ BrowserModule, BrowserAnimationsModule ],
Create an animation.ts file using touch, and then import animation classes and define animation properties:
touch animation.ts
Here is the code for importing and defining animation classes:
import {trigger,state,style,animate,transition,query,animateChild,group} from '@angular/animations'; export const slideInAnimation = trigger('routeAnimations', [ transition('HomePage <=> PostPage', [ style({ position: 'relative' }), query(':enter, :leave', [ style({ position: 'absolute', top: 0, left: 0, width: '100%' }) ]), //[…] query(':enter', animateChild()), ]) ]);
Update the animated route in the lazy loading ap.route.module.tsrouting configuration, as shown in the following code:
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; const routes: Routes = [ { path: 'blog', loadChildren: './blog-home/blog-home.module#BlogHomeModule', data: { animation: 'HomePage' } //[…] @NgModule({ imports: [RouterModule.forChild(routes), SharedModule, BrowserAnimationsModule], exports: [RouterModule] }) export class AppRoutingModule { }
Import the animation and router outlet of the root components class in the app.component.ts file:
import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router' import { slideInAnimation } from './animation' //[…] prepareRoute(outlet: RouterOutlet) { return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation']; }}
Update the root component template file (app.component.html) with the following code:
<div id="main-content" class="bg-color-gray"> <app-header></app-header> <div [@routeAnimations]="prepareRoute(outlet)" class="page-container scene-main scene-main--fade_In"> <router-outlet #outlet="outlet"></router-outlet> <app-footer></app-footer> </div> </div>
Run the application using ng serve –o on the CLI, and then test and observe the page transition on the browser by typing in localhost:4200/blog/post in the browser. You will obtain the following output:
Activity 16: Implementing Router Guard, Constant Storage, and Updating the Application Functions of the Blogging Application
File name: app.routing.module.ts
Live link: http://bit.ly/2XrwQwu
File name: article.service.ts
Live link: http://bit.ly/2GZehKy
File name: blog-home.component.ts
Live link: http://bit.ly/2tDW3GH
File name: blog-home.component.html
Live link: http://bit.ly/2IAhQsY
File name: view-post.component.ts
Live link: http://bit.ly/2ExYdhd
File name: view-post.component.html
Live link: http://bit.ly/2T3cIT9
File name: login.component.ts
Live link: http://bit.ly/2Xl42WN
File name: login.component.html
Live link: http://bit.ly/2UaQATe
File name: register.component.ts
Live link: http://bit.ly/2XqhR60
File name: register.component.html
Live link: http://bit.ly/2IBJoOH
File name: create.component.ts
Live link: http://bit.ly/2GKyDb6
File name: create.component.html
Live link: http://bit.ly/2XqKKPt
File name: edit.component.ts
Live link: http://bit.ly/2Ec53aU
File name: edit.component.html
Live link: http://bit.ly/2BWVjAR
Define a constant for AuthService and ArticleService in the environment.ts file with local URL's as shown in the following snippet:
export const environment = { production: false, articlesUrl: 'http://localhost:3000/articles', articleUrl: 'http://localhost:3000/article/', registerUrl: "http://localhost:3000/auth/register", loginUrl: "http://localhost:3000/auth/sign_in" };
Import and declare the environment in the auth.service.ts file as shown in the following snippet:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http' import { Router } from '@angular/router' import { map } from 'rxjs/operators'; import { environment } from '../../environments/environment' @Injectable({ providedIn: 'root' }) export class AuthService { config = environment;
Import the BehaviorSubject class in the auth.service.ts file using the following command:
import { BehaviorSubject } from 'rxjs';
Declare user as an instance of the BehaviorSubject class and then observe using the Angular asObservable() method as shown in the following snippet:
private user = new BehaviorSubject<boolean>(false); cast = this.user.asObservable();
Write an authentication function in the auth.service.ts file to check if any tokens exist as shown in the following snippet:
constructor(private http: HttpClient, private router: Router) { } public isAuthenticated(): boolean { const token = localStorage.getItem('currentUser'); if (token) return true else return false }
Update the login, register, and logout functions with the constant variable and then observe the behavioral variable as shown in the following snippet:
registerUser(user) { return this.http.post<any>('${this.config.registerUrl}', user) } loginUser(user) { return this.http.post<any>('${this.config.loginUrl}', { 'email': user.email, 'password': user.password }) .pipe(map(user => { // login successful if there's a jwt token in the response if (user && user.token) { // store user details and jwt token in local storage to keep user logged in between page refreshes localStorage.setItem('currentUser', JSON.stringify('JWT ' + user.token)); } this.user.next(true); return user; })); } logoutUser() { // remove user from local storage to log user out localStorage.removeItem('currentUser'); this.user.next(false); this.router.navigate(['/blog']) } }
Create a new auth-guard.service.ts service file to implement the router guard as shown in the following snippet:
import { Injectable } from '@angular/core'; import { Router, CanActivate } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuardService implements CanActivate { constructor(public auth: AuthService, public router: Router) {} canActivate(): boolean { if (!this.auth.isAuthenticated()) { this.router.navigate(['login']); return false; } return true; } }
Apply the router guard service to the app.routing.module.ts route file as shown in the following snippet:
import { NgModule } from '@angular/core'; import { Routes, RouterModule,CanActivate } from '@angular/router'; import { SharedModule } from './shared/shared/shared.module' import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AuthGuardService as AuthGuard } from './service/auth-guard.service'; //[…] // { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [RouterModule.forChild(routes), SharedModule, BrowserAnimationsModule], exports: [RouterModule] }) export class AppRoutingModule { }
Import and declare the environment in the article.service.ts file as shown in the following snippet:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Post } from '../posts' import { Observable } from 'rxjs'; import { environment } from '../../environments/environment' @Injectable() export class ArticleService { config = environment;
Declare the token and header in the article.service.ts file:
article: any; token = JSON.parse( localStorage.getItem('currentUser') ) ; httpOptions:any; constructor(private http: HttpClient) { this.httpOptions = new HttpHeaders({ 'Authorization': this.token, 'Access-Control-Allow-Origin':'*', 'Access-Control-Allow-Methods':'PUT, POST, GET, DELETE, OPTIONS', }); }
Update the functions in the article.service.ts file with constant variables and headers as shown in the following snippet:
getArticles(): Observable<Post> { this.article = this.http.get<Post>('${this.config.articlesUrl}'); return this.article; } //[…] updateArticle(id: number, article: Post): Observable<Post> { console.log(this.token) return this.http.put<Post>('${this.config.articleUrl}' + id, { 'title': article.title, 'body': article.body, 'tag': article.tag, 'photo': article.photo },{ headers: this.httpOptions }) } }
Update the blog-home component class (blog-home.component.ts), template (blog-home.component.html), and style (blog-home.component.css) using the following code snippets:
The code for updating the blog-home.component.ts file is as follows:
//blog-home.component.ts import { Component, OnInit } from '@angular/core'; import { ArticleService } from '../service/article.service'; import { AuthService } from '../service//auth.service'; @Component({ selector: 'app-blog-home', templateUrl: './blog-home.component.html', styleUrls: ['./blog-home.component.css'] }) export class BlogHomeComponent implements OnInit { //[…] } logOut() { this.authService.logoutUser() } }
The code for updating the blog-home.component.html (template) file is as follows:
//blog-home.component.html <app-title-header></app-title-header> <div *ngIf='isLoggedIn'> <a routerLink="/create" style="float: left;margin:-50px 0px 0px 100px;background-color: orangered;color: white" class="button btn">New Post</a> <button (click)="logOut()" style="float: right;margin:-50px 100px 0px 0px;background-color: black;color: white" class="button btn">Logout</button> </div> //[…] </article> </div> </div> </div> </div> </div>
The code snippet for the style (blog-home.component.css) file is as follows:
.button{ border-style: solid; border-width : 1px 1px 1px 1px; text-decoration : none; padding : 8px; font-size:12px; margin: 0 auto; }
Update the view-post component class (view-post.component.ts) and the template (view-post.component.html) using the following code:
The code for updating the view-post.component.ts file is as follows:
//view-post.component.ts import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { ArticleService } from '../service/article.service'; @Component({ selector: 'app-view-post', templateUrl: './view-post.component.html', styleUrls: ['./view-post.component.css'] }) export class ViewPostComponent implements OnInit { //[…] } ); }); } }
The code for updating the view-post.component.html file is as follows:
<!-- View post --> <div class="page-container scene-main scene-main--fade_In"> <!-- Blog post --> <div class="container"> //[…] </p> </div> <div class="separator-line"></div> </article> </div> </div> </div> </div> </div>
Update the login component class (login.component.ts) and the template (login.component.html) using the following code:
The code for updating the login.component.ts file is as follows:
import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms'; import { AuthService } from '../service//auth.service'; import { Router } from '@angular/router' import { first } from 'rxjs/operators'; //[…] } }
The code for updating the login.component.html file is as follows:
<app-title-header></app-title-header> <div class="" style="padding-bottom: 3rem!important;"> <div class="row"> <div class="col-md-6 mx-auto"> <!-- form card login --> <div class="card rounded-0"> <h3 class="mb-0" style="text-align:center" class="mb-0">Login</h3> <div class="card-header"> <a href="#" class="btn" style="float:left;margin-right:10px;color:darkblue;border: 1px solid darkblue"> <i class="fa fa-facebook-official"></i> Facebook //[…] </div> <div *ngIf="success" class="alert alert-success">{{success}}</div> <div *ngIf="error" class="alert alert-danger">{{error}}</div> </form> </div> <!--/card-block--> </div> <!-- /form card login --> </div> </div> </div>
Update the register component class (register.component.ts) and the template (register.component.html) using the following code:
The code for updating the register.component.ts is as follows:
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../service/auth.service'; import { Router } from '@angular/router' import { Users } from '../users'; import { first } from 'rxjs/operators'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.css'] }) //[…] navigateToLogin() { this.router.navigate(['/login']); } }
The code for updating the register.component.html is as follows:
<app-title-header></app-title-header> <div style="padding-bottom: 3rem!important;"> <div class="row"> <div class="col-md-6 mx-auto"> <!-- form card login --> <div class="card rounded-0"> <h3 class="mb-0" style="text-align:center">Register Admin User</h3> //[…] </div> </div> </div>
Update the create component class (create.component.ts) and the template (create.component.html) using the following code snippets:
The code for updating the create.component.ts file is as follows:
import { Component, OnInit } from '@angular/core'; import { Posts } from '../post'; import { ArticleService } from '../service/article.service'; import { Router } from '@angular/router' import { first } from 'rxjs/operators'; //[…] navigateToBlogHome() { this.router.navigate(['/blog']); } }
The code for updating the create.component.html file is as follows:
<app-title-header></app-title-header> <div style="padding-bottom: 3rem!important;"> <div class="row"> <div class="col-md-6 mx-auto"> <!-- form card login --> <div class="card rounded-0"> <h3 style="text-align:center" class="mb-0">Create Post</h3> <div class="card-header"> </div> <div class="card-body"> <form (ngSubmit)="postForm.form.valid && onSubmit()" #postForm="ngForm" novalidate> <div class="form-group"> <label for="title">Title</label> <input type="text" class="form-control" id="title" [(ngModel)]="model.title" name="title" #title="ngModel" [ngClass]="{ 'is-invalid': postForm.submitted && title.invalid }" required> <div *ngIf="postForm.submitted && title.invalid" class="alert alert-danger"> Title is required </div> </div> //[…] <div *ngIf="error" class="alert alert-danger">{{error}}</div> </form> </div> </div> </div> </div> </div>
Update the edit component class (edit.component.ts) and the template (edit.component.html) using the following code snippets:
The code for updating the edit.component.ts file is as follows:
import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { ArticleService } from '../service/article.service'; import { Posts } from '../post'; import { first } from 'rxjs/operators'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.css'] }) //[…] navigateToBlogHome() { this.router.navigate(['/blog']); } }
The code for updating the edit.component.html file is as follows:
<app-title-header></app-title-header> <div style="padding-bottom: 3rem!important;"> <div class="row"> <div class="col-md-6 mx-auto"> <!-- form card login --> <div class="card rounded-0"> <h3 style="text-align:center" class="mb-0">Edit Post</h3> <div class="card-header"> </div> <div class="card-body"> <form (ngSubmit)="postForm.form.valid && onSubmit()" #postForm="ngForm" novalidate *ngIf="article.length != 0"> <div class="form-group"> <label for="title">Title</label> <input type="text" class="form-control" id="title" [(ngModel)]="article.title" name="title" #title="ngModel" [ngClass]="{ 'is-invalid': postForm.submitted && title.invalid }" required> <div *ngIf="postForm.submitted && title.invalid" class="alert alert-danger"> Title is required </div> </div> //[…] </form> </div> </div> </div> </div> </div>
Run ng serve to test the '/blog' route before and after logging in.
You will obtain the following output once you test the '/blog' route before logging in:
When you test the '/blog' route after logging in, you will obtain the following output:
Activity 17: Performing Unit Testing on the App Root Component and Blog-Post Component
Open the root component test file, app.components.spec.ts, and import the modules, as shown:
import { TestBed, async,ComponentFixture } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { Component, OnInit, DebugElement } from '@angular/core'; import { RouterTestingModule } from '@angular/router/testing'; import { RouterLinkWithHref } from '@angular/router'; import { By } from '@angular/platform-browser';
Mock the app-header, router-outlet, and app-footer components into the app.components.spec.ts file the with the following code:
@Component({selector: 'app-header', template: ''}) class HeaderStubComponent {} @Component({selector: 'router-outlet', template: ''}) class RouterOutletStubComponent { } @Component({selector: 'app-footer', template: ''}) class FooterStubComponent {}
Write the suits functions for AppComponent, as shown in the following code:
describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ RouterTestingModule.withRoutes([])], declarations: [ AppComponent, HeaderStubComponent, RouterOutletStubComponent, FooterStubComponent ] }).compileComponents(); }));
Write the assertion and matcher functions to evaluate true and false conditions:
it('should create the app', async(() => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); })); it('should have a link to /', () => { const fixture = TestBed.createComponent(AppComponent); const debugElements = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); const index = debugElements.findIndex(de => { return de.properties['href'] === '/'; }); expect(index).toBeGreaterThanOrEqual(-1); }); });
Open the blog-home.component.spec.ts file (in the blog-home folder) and import the modules:
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing'; import { HttpClientModule } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; import { BlogHomeComponent } from './blog-home.component'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ArticleService } from '../service/article.service';
Mock the app-title-header components in the blog-home.component.spec.ts file, as shown:
@Component({ selector: 'app-title-header', template: '' }) class TitleHeaderStubComponent { }
Write the suits functions in the blog-home.component.spec.ts file, as shown:
describe('BlogHomeComponent', () => { let component: BlogHomeComponent; let fixture: ComponentFixture<BlogHomeComponent>; let testBedService: ArticleService; beforeEach(async(() => { // refine the test module by declaring the test component TestBed.configureTestingModule({ declarations: [BlogHomeComponent, TitleHeaderStubComponent], providers: [ArticleService], imports: [HttpClientModule], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); })); beforeEach(() => { // create component and test fixture fixture = TestBed.createComponent(BlogHomeComponent); // AuthService provided to the TestBed testBedService = TestBed.get(ArticleService); // get test component from the fixture component = fixture.componentInstance; fixture.detectChanges();
Write the assertion and matcher functions to evaluate true and false, as shown:
it('should create', () => { expect(component).toBeTruthy(); }); it('Service injected via inject(...) and TestBed.get(...) should be the same instance', inject([ArticleService], (injectService: ArticleService) => { expect(injectService).toBe(testBedService); }) ); }); });
Run ng test in the command line. You will obtain an output similar to the following:
As can be seen in the preceding output, we have successfully performed e2e testing on the student app.