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

JS Utils

JWT Token

Intereptor to Refresh Token
Ex1: https://medium.com/@alexandrubereghici/angular-tutorial-implement-refresh-token-with-httpinterceptor-bfa27b966f57

Ex2: https://codeforgeek.com/2018/03/refresh-token-jwt-nodejs-authentication/
Ex3: https://gist.github.com/Toilal/8849bd63d53bd2df2dd4df92d3b12f26

Minimalist Example:
Ex1: http://jasonwatmore.com/post/2018/05/23/angular-6-jwt-authentication-example-tutorial

Prevent SQL Injection

RegEx Playground: https://regex101.com/

  • 2 int followed by 4 caps: ^[A-Z]{2}[0-9]{4}$
  • Example2: ^[A-Za-z ][A-Za-z0-9!@#$%^&*’ ]{1,20}$

str.replace(/<[^>]*>/g, ”)

var str = '
<h2>Hello</h2>
';
str.replace(/]*&gt;/g, '');

Sort array of objects by ID

var data = [1, 2, 3, 4, 5]
  .map(el => {
    return {
      id: el,
      name: 'alpha',
      title: 'Mr.',
      description: 'laziest objects',
      age: (el * 10)
    };
  });
data.sort(function(a, b){return b.id - a.year});

Date Diff

d2 = new Date(2019, 3, 18);
d1 = new Date(2018, 9, 18);

Date.diffInMonths = function(d1, d2) {
  return Math.round((d2 - d1) / (60 * 30 * 60 * 24 * 1000));
}

Date.DateDiff = {

    inDays: function(d1, d2) {
        var t2 = d2.getTime();
        var t1 = d1.getTime();

        return parseInt((t2-t1)/(24*3600*1000));
    },

    inWeeks: function(d1, d2) {
        var t2 = d2.getTime();
        var t1 = d1.getTime();

        return parseInt((t2-t1)/(24*3600*1000*7));
    },

    inMonths: function(d1, d2) {
        var d1Y = d1.getFullYear();
        var d2Y = d2.getFullYear();
        var d1M = d1.getMonth();
        var d2M = d2.getMonth();

        return (d2M+12*d2Y)-(d1M+12*d1Y);
    },

    inYears: function(d1, d2) {
        return d2.getFullYear()-d1.getFullYear();
    }
}

Date.DateDiff.inDays(d1,d2);

 

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}}