Angular Testing

Testing a Simple Component

import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent Tests', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));

  it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));

  it('should render greeting', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain('Welcome!');
  }));
});

Testing Template


// EX1
import { async, TestBed } from '@angular/core/testing';
import { SomeComponent } from './some.component';

beforeEach(() => {
  TestBed.configureTestingModule({
    declarations: [
      SomeComponent
    ],
    imports: [
      // HttpModule, etc.
    ],
    providers: [
      // { provide: ServiceA, useClass: TestServiceA }
    ]
  });
});

it('should do something', async(() => {
  TestBed.compileComponents().then(() => {
    const fixture = TestBed.createComponent(SomeComponent);

    // Access the dependency injected component instance
    const app = fixture.componentInstance;
    expect(app.something).toBe('something');

    // Access native element
    const element = fixture.nativeElement;

    // detect changes
    fixture.detectChanges();
    expect(element.textContent).toContain('something');
  });
}));


// EX2
it('should get quote', () => {

  fixture.debugElement.componentInstance.getQuote();
  fixture.detectChanges();
  var compiled = fixture.debugElement.nativeElement;

  expect(compiled.querySelector('div'))
    .toHaveText('Test Quote'); 
});
​

// EX3
import {TestBed, ComponentFixture, inject, async} from '@angular/core/testing';
import {LandingComponent, User} from './landing.component';
import {Component, DebugElement} from "@angular/core";
import {By} from "@angular/platform-browser";


describe('Component: Landing', () => {

  let component: LoginComponent;
  let fixture: ComponentFixture;
  let submitEl: DebugElement;
  let usernameEl: DebugElement;

  beforeEach(() => {

    // refine the test module by declaring the test component
    TestBed.configureTestingModule({
      declarations: [LandingComponent]
    });


    // create component and test fixture
    fixture = TestBed.createComponent(LandingComponent);

    // get test component from the fixture
    component = fixture.componentInstance;

    submitEl = fixture.debugElement.query(By.css('button'));
    usernameEl = fixture.debugElement.query(By.css('input[type=text]'));
  });

  it('Setting enabled to false disabled the submit button', () => {
    component.enabled = false;
    fixture.detectChanges();
    expect(submitEl.nativeElement.disabled).toBeTruthy();
  });

  it('Setting enabled to true enables the submit button', () => {
    component.enabled = true;
    fixture.detectChanges();
    expect(submitEl.nativeElement.disabled).toBeFalsy();
  });

  it('Entering email and password emits loggedIn event', () => {
    let user: User;
    usernameEl.nativeElement.value = "test";

    // Subscribe to the Observable and store the user in a local variable.
    component.loggedIn.subscribe((value) => user = value);

    // This sync emits the event and the subscribe callback gets executed above
    submitEl.triggerEventHandler('click', null);

    // Now we can check to make sure the emitted value is correct
    expect(user.name).toBe("test");
  });
});


Test a Simple Class


import { countries } from './countries';
import { Util } from './util';

let testClass: Util = null;
describe('Util Tests', () => {
  beforeEach(() => {
    testClass = new Util();
  });

  it('should ensure getCountries is based on countries file', () => {
    const actual = testClass.getCountries;
    expect(actual[0].id).toEqual(countries[0].id);
  });
});

import { countries } from './countries';
export class Util {

  public countries: any[] = countries;

  public getProducts() {
    return this.countries;
  }
}

Mock Event

  const mockEvent: Event = {
    srcElement: {
      classList: ''
    },
    charCode: 64,
    stopPropagation: ( ( e: any ) => { /**/ }),
    preventDefault: ( ( e: any ) => { /**/ }),
  };

Service Testing
https://semaphoreci.com/community/tutorials/testing-services-in-angular-2
https://blog.realworldfullstack.io/real-world-angular-part-9-unit-testing-c62ba20b1d93
https://stackoverflow.com/questions/42440234/unit-test-a-angular2-service-that-uses-rxjs-subjects

Template Testing
https://codecraft.tv/courses/angular/unit-testing/components/

GAnalytics
https://codeburst.io/using-google-analytics-with-angular-25c93bffaa18

Advertisements

Response Mapping

REST API
http://jsonplaceholder.typicode.com/users/

export class MyApp {
  private users = [];

  constructor(http: Http) {
    http.get('http://jsonplaceholder.typicode.com/users/')
        .flatMap((response) => response.json())
        .filter((person) => person.id > 5)
        .map((person) => "Mr. " + person.name)
        .subscribe((data) => {
          this.users.push(data);
        });
  }
}


On Console:
var users = [];
[
  {
    "id": 2,
    "name": "Ervin Howell",
    "username": "Antonette",
    "email": "Shanna@melissa.tv",
    "address": {
      "street": "Victor Plains",
      "suite": "Suite 879",
      "city": "Wisokyburgh",
      "zipcode": "90566-7771"
    },
    "phone": "010-692-6593 x09125",
    "website": "anastasia.net"
  },
  {
    "id": 3,
    "name": "Clementine Bauch",
    "username": "Samantha",
    "email": "Nathan@yesenia.net",
    "address": {
      "street": "Douglas Extension",
      "suite": "Suite 847",
      "city": "McKenziehaven",
      "zipcode": "59590-4157"
    },
    "phone": "1-463-123-4447",
    "website": "ramiro.info"
  },
  {
    "id": 5,
    "name": "Chelsey Dietrich",
    "username": "Kamren",
    "email": "Lucio_Hettinger@annie.ca",
    "address": {
      "street": "Skiles Walks",
      "suite": "Suite 351",
      "city": "Roscoeview",
      "zipcode": "33263"
    },
    "phone": "(254)954-1289",
    "website": "demarco.info"
  },
  {
    "id": 6,
    "name": "Mrs. Dennis Schulist",
    "username": "Leopoldo_Corkery",
    "address": {
      "street": "Norberto Crossing",
      "suite": "Apt. 950",
      "city": "South Christy",
      "zipcode": "23505-1337"
    },
    "phone": "1-477-935-8478 x6430",
    "website": "ola.org"
  }
].map((response) => response)
        .filter((person) => person.id > 3)
        .map((person) => { 
          return {
            name: 'Mr.' + person.name,
            company: 'laziest approach to constructing objs',
            id: person.id
          };
        })
        .map((data) => {
          users.push(data);
        });

Callbacks

function eat(at, callback) {
  setTimeout(function() {
    console.log('3. In 3 secs');
  }, 3000);
  console.log(`1. eat at ${at} & `);
  callback(5);
}

function sleep(at){
  setTimeout(function() {
    console.log(`2. sleep at ${at}`);
  }, 2000);
}

eat(3, sleep);

/> This does nothing

function greetWorld() {
  console.log(new Date() + ': Hello, world!');
}
var timerId = setTimeout(greetWorld, 2000);
clearTimeout(timerId);

Ex2:

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
function log() {
  console.log('LG');
}

async function demo() {
  setTimeout("console.log('first');", 5000);
  await sleep(5000);
  console.log('second one');log();
}
demo();

// call books api
this.bookService.getBooks()
  .subscribe(
    response => {
      this.booksLoaded = true;
      this.books = response;
    },
    err => {
      console.log(‘books failed to load: ' + JSON.stringify(err));
  });

Angular Survival Kit

https://stackoverflow.com/questions/35435042/how-can-i-define-an-array-of-objects-in-typescript

https://stackoverflow.com/questions/29382389/defining-array-with-multiple-types-in-typescript

https://basarat.gitbooks.io/typescript/docs/types/generics.html

Angular SwitchCase

<!div *ngFor="let id of ids">Id is {{id}}
  <!div ngSwitch="{{id%2}}">
  <!div *ngSwitchCase="'0'" [ngClass]="'one'">I am Even.<!/div>
  <!div *ngSwitchCase="'1'" [ngClass]="'two'">I am Odd.<!/div>
  <!div *ngSwitchDefault>Nothing Found. <!/div>

<!/div> <!/div>

Testing Localstorage

  spyOn(localStorage.__proto__, 'removeItem');
  component.removeToken();
  expect(localStorage.__proto__.removeItem).toHaveBeenCalledTimes(2);

Testing Form Controls

it('submitting a form emits a user', () => {
    expect(component.form.valid).toBeFalsy();

    component.form.controls['email'].setValue("test@test.com");
    component.form.controls['phone'].setValue("111222333");
    expect(component.form.valid).toBeTruthy();

    let user: User;
    // Subscribe to the Observable and store the user in a local variable.
    component.loggedIn.subscribe((value) => user = value);

    // Trigger the login function
    component.submit();

    // Now we can check to make sure the emitted value is correct
    expect(user.email).toBe("test@test.com");
    expect(user.phone).toBe("111222333");
  });

Testing Util Class (Non Static methods)

import { countries } from ‘./countries’;
import { Util } from ‘./util’;

let testClass: Util = null;
describe(‘Util Tests', () => {
  beforeEach(() => {
    testClass = new Util();
  });

  it('should ensure getCountries is based on countries file', () => {
    const actual = testClass.getCountries;
    expect(actual[0].id).toEqual(countries[0].id);
  });
});

import { countries } from ‘./countries’;
export class Util {

  public countries: any[] = countries;
  public getProducts() {
    return this.countries;
  }
}

Force Change Detection

import { ChangeDetectorRef } from '@angular/core';

constructor(
  private ref: ChangeDetectorRef
) {}

this.ref.markForCheck();
this.ref.detectChanges();

Check if already selected item was clicked

ts:
public onUpdate(event, index) {
  const preventAction = event
    .target.classList.contains('selected');
  if(!preventAction) {
    // handle the event that got fired!
  }
}
tpl:
(click)="onUpdate($event, i)"

jQuery:
$(document).ready(function() {
    $("a").click(function(event) {
        console.log(event.target.class);
    });
});

Routing & Dynamic Form Controls setup


constructor(private router: Router) {}

this.arryUsers.forEach(user=&gt;{
  this.userForm.addControl(user.id , control);
});

goHome() {
    this.router.navigate(['']);
}

<!-- *ngFor="let item of items; let i = index" -->

Form State

onChanges(): void {
  this.myForm.valueChanges.subscribe(val => {
    console.log(val);
  });
}

onSubmit() {
  if (this.myform.valid) {
    console.log("Form Submitted!");
  }
}
tpl:
{{myform.value | json}}

Promises Basic

// Promises Basic
var p1 = Promise.resolve("calling next");
p1.then((res)=>console.log(res));

JSON Filters

// lookup attributes for a value
vendors = [{
      Name: 'Apple',
      ID: 'APL'
    },
    {
      Name: 'Microsoft',
      ID: 'MST'
    }];

# solution 1 ES6 Arrow
vendors.filter(e => e.Name === 'Microsoft');

# solution 2 only till 1st result is found
vendors.find(item => {
    return item.Name === 'Microsoft';
});

Test DOM Elements with Jasmine

// Jasmine query selector by id
de = fixture.debugElement.query(by.css('#main-container'));

import { TestBed, async, ComponentFixture } from '@angular/core/testing';

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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ ],
      declarations: [ TestComponent ],
      providers[  ]
    }).compileComponents().then(() => {
      fixture = TestBed.createComponent(TestComponent);
      component = fixture.componentInsance;
    });
  }));
});

it('should', async(() => {
  spyOn(component, 'onEditButtonClick');

  let button = fixture.debugElement.nativeElement.querySelector('button');
  button.click();

  fixture.whenStable().then(() => {
    expect(component.onEditButtonClick).toHaveBeenCalled();
  })
}));

OR

it('should', fakeAsync( () => {
    fixture.detectChanges();
    spyOn(componentInstance, 'method name'); //method attached to the click.
    let btn = fixture.debugElement.query(By.css('button'));
    btn.triggerEventHandler('click', null);
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(componentInstance.methodName).toHaveBeenCalled();
}));

Typescript Getters

these shorthands really help to get rid of long getters that you have to write in order to access a from property.

export class SignupFormComponent {
  get email() {
    return this.signupForm.get('email');
  }

  get password() {
    return this.signupForm.get('password');
  }

  get terms() {
    return this.signupForm.get('terms');
  }
}

Re-render Child Component

Sometimes updating @input doesn’t guarantee a re-render of child components. Here is an approach to resolve it.

export class ChildComponent implements OnChanges {
  @Input() data: Data;
  @ViewChild(‘childComp’) childComp: ElementRef;

  ngOnChanges(changes: SimpleChanges) {
    this.updateData();
  }
}<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>

Proxy Conf in Angular 2+

in case, if we run mock apis on http://localhost:3000/api & want all calls to http://localhost:4200/api reach the first (mock) server.

DOC - https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/proxy.md

Serve Angular on XAMPP (apache) server

change in my root directory index.html file which is:

<base href="/"> to <base href="./">

Now build the app:


/> ng build --prod
copy dist folder and paste it in my xampp htdocs folder and access the site using:

localhost:8080/dist/

Ref:

https://codecraft.tv/courses/angular/http/http-with-promises/
https://scotch.io/tutorials/how-to-deal-with-different-form-controls-in-angular-2
https://ciphertrick.com/2017/07/24/parent-child-component-communication-angular/
https://toddmotto.com/angular-dynamic-components-forms
https://scotch.io/tutorials/3-ways-to-pass-async-data-to-angular-2-child-components

View story at Medium.com
http://marclloyd.co.uk/angular2/spying-on-router-navigate-in-angular-2-unit-tests/

https://vsavkin.com/three-ways-to-test-angular-2-components-dcea8e90bd8d

HighCharts: https://medium.com/@balramchavan/using-highcharts-with-angular-5-6c6564a55cf0

Aside

Angular5 part-3

a minimalist quiz component in Angular5 in less than 100 lines of code (including its data).

/> Typescript source

import { Component } from '@angular/core';

@Component({
  selector: 'app-authors',
  templateUrl: './authors.component.html',
  styleUrls: ['./authors.component.css']
})
export class AuthorsComponent {

  // maintains the state of the quiz
  private queId: number = -1;
  private current: number = 0;

  // capture user interaction
  public user: any = {};
  private responses: any = [];

  questions: any = [{
            question: 'Which is the largest country in the world by population?',
            options: ['India', 'USA', 'China', 'Russia'],
            answer: 3
        }, {
            question: 'When did the second world war end?',
            options: ['1945', '1939', '1944', '1942'],
            answer: 1
        }, {
            question: 'Which was the first country to issue paper currency?',
            options: ['USA', 'France', 'Italy', 'China'],
            answer: 4
        }, {
            question: 'Which city hosted the 1996 Summer Olympics?',
            options: ['Atlanta', 'Sydney', 'Athens', 'Beijing'],
            answer: 1
        }, {
            question: 'Who invented telephone?',
            options: ['Albert Einstein', 'Alexander Graham Bell', 'Isaac Newton', 'Marie Curie'],
            answer: 1
        }];

  constructor() {
    this.queId = this.getRandom(0, 4);
    this.user.response = '';
    this.user.score = 0;
    this.user.responses = [];
    this.updateState();
  }

  get isQuizMode() {
      return (this.responses.length <= 4);
  }

  get score(): number {
    return this.user.score;
  }

  onNext() {
    event.preventDefault();
    if (this.current = 2) {
         if (this.responses.length < this.current) {
             this.responses.push({'response': this.user.response, 'queId': this.queId});
         }
         this.queId = this.responses[this.current - 2].queId;
         this.user.response = this.responses[this.current - 2].response;
         this.current--;
     }// console.log(this.responses);
  }

  updateState (): void {
      if (!this.isQuizMode) {
          for (let i = 0; i < 5; i++) {
              let usrRes = this.responses[i].response;
              let ans = this.questions[this.responses[i].queId].answer;
              if (usrRes == ans) {
                  this.user.score++;
              }
          }
          this.user.score *= 20;
      } else {
          this.current++;
      }
  }

  getRandom(min, max): number {
      return Math.floor(Math.random() * (max - min + 1)) + min;
  }
}

/> The Markup


<!-- render questions //-->
<div>
    
<div style="text-align:left;">
            {{current}}. {{questions[queId].question}}
<ul>
	<li>{{questions[queId].options[0]}}</li>
	<li>{{questions[queId].options[1]}}</li>
	<li>{{questions[queId].options[2]}}</li>
	<li>{{questions[queId].options[3]}}</li>
</ul>
</div>
Next
        Prev
    </div>
<!-- show summary //-->

<h2>Results</h2>
<h3>Your Score: {{score}}</h3>
<p>
            {{i+1}}. queid: {{questions[resp.queId].question}}
correct ans: {{questions[resp.queId].answer}}
            user sel: {{resp.response}}


Angular5 part-2

There is only word which is synonymous with Angular, C H A N G E. Few months ago I made a post on Angular2 & now we have landed on Angular5. It is definitely challenging to keep us on par with the latest in the Angular world.

So to keep my dice rolling, I decided to make my angular posts into a series. In this we look at debugging our unit tests on a browser. In Javascript world, we all know and/or have used Jasmine for Unit Testing at some point.

By default it runs on a headless browser (usually PhantomJS) so we launch the test case/suite from terminal & just wait till test results but we never get to see anything!

But if we change this to a browser like Chrome, we could actually see how the test cases are being executed. Just like UI automation tests, fro example, this would show us text being inserted on forms, button being clicked & etc. Without no more explaination, let’s dive into how we establish this; Just 3 steps!

01 | Get the karma chrome launcher for your project

npm install karma-chrome-launcher --save-dev

02 | Modify karma.conf.json to add the following

    customLaunchers: {
      Chrome_without_security: {
        base: 'Chrome',
        flags: ['--disable-web-security']
      }
    },
    broswers [
        'phantomJS','Chrome', 'Chrome_without_security'
    ]

03 | add the following script definition to package.json

"test-watch": "node node_modules/karma/bin/karma start --browsers=Chrome,Chrome_without_security --single-run=true --auto-watch",

That is it!

Now open up terminal from your project path and hit the following and see magic happen on chrome (look for port on the output)

npm run test-watch

Angular5 part-1

01 | Template on Plunker

https://plnkr.co/edit/gQjnRg?p=info

02 | Setup a project locally using ng-cli

if you already have an older version:

npm uninstall -g angular-cli
npm cache clean
npm install -g @angular/cli@latest

03 | Configure Routes

WARNING! AppComponent Shouldn’t be part of routing. But it is set as the default route by ng-cli.

> Never Use this:

{ path: '', component: AppComponent },

> Should use something like below:

import { Routes } from '@angular/router';
import { AppComponent } from './';
import { ContactComponent } from './contact/contact.component';
import { AboutComponent } from './about/about.component';

export const routes: Routes = [
  { path: '', redirectTo: '/contact', pathMatch: 'full' },
  { path: 'contact',  component: ContactComponent },
  { path: 'about',  component: AboutComponent },
  { path: '**', redirectTo: '/contact', pathMatch: 'full' },
];
Route Redirection:
  { path: '**', redirectTo: '/contact', pathMatch: 'full' },
Wildcard Routes:
  { path: '**', component: PageNotFoundComponent },

04 | Routing

From Template:

<a>Click here to Signup</a>

From your .ts file (typescript):

// src/app/landing/landing.component.ts
import { Component } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { Router } from '@angular/router';

@Component({
  selector: 'app-root',
  templateUrl: './landing.component.html',
  styleUrls: ['./landing.component.css']
})
export class LandingComponent {
  title = 'landing page';
  constructor(private router: Router) {}

  launchHome(): void {
    this.router.navigate(['/home']);
  }
}

05 | Barrel Files for the rescue

One of the first things you will notice when you start working on it is, you have to write so many import statements. Gets worst, when you have to make 2 dedicated imports to get hold of 2 components sitting inside the same folder.

Well, this isn’t one of those where you blame at Angular! This is something to do with Typescript. The resolution is to use barrel files. As a practice you can start creating an index.ts file in each of your folders containing services, components or feature modules.

Assume you src folder has too many components (example: Home, About, Contact):

The barrel file:

// src/app/index.ts
export { AppComponent } from './app.component';
export { HomeComponent } from './home.component';
export { AboutComponent } from './about.component';
export { ContactComponent } from './contact.component';

Usage:

import { AppComponent, HomeComponent, AboutComponent, ContactComponent} form './';

// or even simpler
import { * } from './';

There is a neat blog on this which can give you a better understanding of this.

06 | Running the App

ng serve

and navigate your browser to http://localhost:4200

07 | Unit Test and other CI/CD Concerns

There are scripts setup on package.json for these, one can refer it. For more on configuring karma & etc read this blog. Example, Running unit tests (jasmine – BDD):

ng test

08 | Need to create your feature module?

After sometime you realize your app module has grown out of proportion. It is time to consider some logical organization of your code. First is to consider organizing them into modules. Here is a neat read

09 | Adding a Simple Form

You will notice I’ve placed a simple form in ContactComponent. At this point it’s important that you don’t forget to import FormsModule in your app.module.ts.

However, one thing you will notice there is a realtime representation of form data. It is quite useful for you to debug, it can be achieved with the following peace of markup:


    
      ...
    
<div>Formdata in realtime:
<pre>{{ form.value | json }}</pre>
</div>

10 | Lifecycle Hooks

Intention behind post was to have an Angular2 guide which would help one to start a project from scratch & take it all way to deployment. I haven’t covered much of Angular2 concepts or structs on this post for brevity & aligning with my intention to keep it super short. However, Lifecycle Hooks are a thing it can’t afford to miss. Angular2 provides 8 of them in total: ngOnInit, ngOnChanges, ngDoCheck, ngAfterContentInit, ngAfterContentChecked, ngAfterViewInit, ngAfterViewChecked, ngOnDestroy. Here is an example:

import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit, OnDestroy {
  constructor() {}

  ngOnInit() {
    console.log('App Component Init');
  }

  ngOnDestroy() {
    console.log('App Component Destroyed');
  }
}

You can refer this blog if you want to know them in detail

11 | Before you go for Production

You are just a command away if you want to do a production build for your awesome app. Afterwards you just need to copy the generated dist folder to your live server.

ng build --aot --environment prod

Yet, there are few things one should know going to this step. When you execute above the step there are so many things happening behind the scene. Here are some, concatenation, minification, uglification & tree shaking.

Since I have already made a note on first 3 on a different blog, I would like you to come to terms with Tree Shaking.

Tree Shaking is the process of getting rid of dead code. If you’re coming from Java world or most other OOP environments, you will be familiar with a warning called ‘unused code/method’. However, if you want to go deep into it with examples & numbers here is great post from an expert. Minko Gechev is the author of the official Angular Style Guide.

If you are already an expert at Angular & came across this post whilst considering ways to improve the page load time & performance tuning for your app here is a great stack overflow thread which you shouldn’t miss!

12 | Sourcecode

Here is the github repo used for this post.