<!DOCTYPE html>
<html>

  <head>
    <!-- jasmine -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/1.3.1/jasmine.js"></script>
    <!-- jasmine's html reporting code and css -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/1.3.1/jasmine-html.js"></script>
    <link href="//cdnjs.cloudflare.com/ajax/libs/jasmine/1.3.1/jasmine.css" rel="stylesheet" />
    <!-- angular itself -->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular.js"></script>
    <!-- angular's testing helpers -->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular-mocks.js"></script>
    
    <script src="myApp.js"></script>
    <script src="myHttpController.js"></script>
    <script src="mySvcApp.js"></script>
    
    <script src="test/myMathSvcSpec.js"></script>
    <script src="test/myMathControllerSpec.js"></script>
    <script src="test/myThisControllerSpec.js"></script>
    <script src="test/myHttpControllerSpec.js"></script>
    <script src="test/myMathSvcSpec.js"></script>
    <style>
      .jasmine_reporter > .results > .summary > 
        .suite.passed > .description {
	      background-color: #a6b779;
	      padding: 3px;
	      color: #fff;
      }
    </style>
  </head>

  <body>
    <h1>Unit Testing AngularJS with Jasmine</h1>
    <h5> - By Paritosh!</h5>
    <a taget="_blank" href="https://simplifyingtechblog.wordpress.com/2017/06/13/unit-testing-angularjs-with-jasmine/">
      Open the blog
    </a>
    <hr/>
    
    <!-- bootstrap jasmine! -->
    <script>
  var jasmineEnv = jasmine.getEnv();
  
  // Tell it to add an Html Reporter
  // this will add detailed HTML-formatted results
  // for each spec ran.
  jasmineEnv.addReporter(new jasmine.HtmlReporter());
  
  // Execute the tests!
  jasmineEnv.execute();
  </script>
  </body>

</html>
var mySvcApp = angular.module('mySvcApp', []);

mySvcApp.service('myHttpSvc', ['$http', function($http){
	this.getServerData = function(){
		return $http.get('myData.json');
	};

	this.addData = function(name){
		return $http.post('postMethod', { 'name': name }, {});
	};
}]);
mySvcApp.service('mySharedDataSvc', [function(){
	var myProperty;

	this.setMyProperty = function(value){
		this.myProperty = value;
	};
	this.getMyProperty = function(value){
		return this.myProperty;
	};
}]);
mySvcApp.service('myMathSvc', [function(){
	this.add = function(a,b){
		if(isNaN(a) || isNaN(b)){
			throw new Error('invalid parameters');
		}
		return a+b;
	}
	this.divide = function(a,b){
		if(isNaN(a) || isNaN(b)){
			throw new Error('invalid parameters');
		}
		if(b == 0){
			throw new Error('divide by zero error');
		}
		return a/b;
	}
}]);
var myApp = angular.module('myApp', ['mySvcApp']);
myApp.controller('myMathController', 
	['$scope', '$window', '$log', 'myMathSvc', 
	function($scope, $window, $log, myMathSvc){

		$scope.param1 = 0;
		$scope.param2 = 0;
		$scope.result = 0;

		$scope.addition = function(){
			$scope.result = myMathSvc.add($scope.param1, $scope.param2);
		};

		$scope.division = function(){
			try{
				$scope.result = myMathSvc.divide($scope.param1, $scope.param2);
			}
			catch(e){
				$window.alert(e.message);
				$log.log(e);
				$scope.result = '';
			}
		};
	}]
);
myApp.controller('myThisController', 
	['$scope', '$timeout', 'myMathSvc', 
	function($scope, $timeout, myMathSvc){

		this.param1 = 0;
		this.param2 = 0;
		this.result = 0;

		this.addition = function(){
			this.result = myMathSvc.add(this.param1, this.param2);
		};

		this.delayedAddition = function(){
			var tempResult = myMathSvc.add(this.param1, this.param2);
			$timeout(function(){
				this.result = tempResult;
			});
		};

	}]
);
describe('testing myMathController', function(){

	var $controller, scope;
	var ctrl, getController;
	var windowMock, logMock, myMathSvc;

	beforeEach(function(){
		// Step 1. load the modules we need
		module('mySvcApp');
		module('myApp');

		inject(function($injector){
			$controller = $injector.get('$controller');
			
			// Step 2. load/create external dependencies to inject
			scope = $injector.get('$rootScope').$new();
			myMathSvc = $injector.get('myMathSvc');

			windowMock = {
				alert: function(msg){}
			};
			logMock = {
				log:  function(msg){}
			};

    		// Step 3. inject the dependencies
			getController = function(){
				return $controller('myMathController', {
					'$scope': scope,
					'$window': windowMock,
					'$log': logMock,
					'myMathSvc': myMathSvc
				});
			};

			ctrl = getController();
		});

	});

	it('1. should load controller properly', function(){
		expect(ctrl).not.toBeNull();
		expect(ctrl).not.toBe(undefined);

		expect(scope.param1).toBe(0);
		expect(scope.param2).toBe(0);
	});

	it('2. should call math service to add operands user has provided', function(){
		
		// Step 4. prepare the data / set spies
		var op1 = 5, op2 = 2, fakeResult = 7;
		spyOn(myMathSvc, 'add').andReturn(fakeResult);

		scope.param1 = op1;
		scope.param2 = op2;
		scope.addition();
		scope.$digest();

    	// Step 5. now test the result we are expecting
		expect(myMathSvc.add).toHaveBeenCalled();
		expect(myMathSvc.add).toHaveBeenCalledWith(scope.param1, scope.param2);
		expect(scope.result).toBe(fakeResult);

		//other way to get call details
		expect(myMathSvc.add.calls.length).toBe(1);
		expect(myMathSvc.add.calls[0].args[0]).toBe(scope.param1);
		expect(myMathSvc.add.calls[0].args[1]).toBe(scope.param2);
		expect(myMathSvc.add.mostRecentCall.args[0]).toBe(scope.param1);
		expect(myMathSvc.add.mostRecentCall.args[1]).toBe(scope.param2);

		//ok, call it once more - want to see few more stuff
		scope.param1 = 123;
		scope.param2 = 234;
		scope.addition();
		scope.$digest();
		expect(myMathSvc.add.mostRecentCall.args[0]).toBe(123);
		expect(myMathSvc.add.mostRecentCall.args[1]).toBe(234);
		
		//do you want to change the fake call result to anything else??
		//no problem. We have got it covered
		myMathSvc.add.isSpy = false;
		spyOn(myMathSvc, 'add').andReturn(5000);
		
		expect(myMathSvc.add.calls.length).toBe(0);
		scope.addition();
		scope.$digest();
		expect(myMathSvc.add).toHaveBeenCalled();
		expect(myMathSvc.add.calls.length).toBe(1);
		expect(scope.result).toBe(5000);
	});

	describe('testing division operation', function(){

		it('3. should call math service to divide operands use has provided', function(){
			var op1 = 5, op2 = 2, fakeResult = 7;
			spyOn(myMathSvc, 'divide').andReturn(fakeResult);

			scope.param1 = op1;
			scope.param2 = op2;
			scope.division();
			scope.$digest();

			expect(myMathSvc.divide).toHaveBeenCalledWith(scope.param1, scope.param2);
			expect(scope.result).toBe(fakeResult);
		});

		it('4. should alert/log if operands are not provided properly', function(){
			var errorMsg = 'Fake Error Message';
			spyOn(myMathSvc, 'divide').andThrow(new Error(errorMsg));
			spyOn(windowMock, 'alert');
			spyOn(logMock, 'log');

			scope.param1 = 10;
			scope.param2 = 'abc';
			scope.division();
			scope.$digest();

			expect(myMathSvc.divide).toHaveBeenCalledWith(scope.param1, scope.param2);
			expect(windowMock.alert).toHaveBeenCalledWith(errorMsg);
			expect(logMock.log).toHaveBeenCalled();
		});
	});

});
describe('testing myHttpController', function(){

	var $controller, scope, $compile;
	var ctrl, getController;
	var windowMock, logMock, myHttpSvc;

	beforeEach(function(){
		module('mySvcApp');
		module('myApp');

		inject(function($injector){
			$compile = $injector.get('$compile');
			$controller = $injector.get('$controller');
			scope = $injector.get('$rootScope').$new();
			myHttpSvc = $injector.get('myHttpSvc');

			logMock = {
				log:  function(msg){}
			};

			getController = function(){
				return $controller('myHttpController', {
					'$scope': scope,
					'$log': logMock,
					'myHttpSvc': myHttpSvc
				});
			};

			ctrl = getController();
		});

	});

	it('1. should load controller properly', function(){
		expect(ctrl).not.toBeNull();
		expect(ctrl).not.toBe(undefined);

		expect(angular.equals(scope.myData, [])).toBe(true);
	});

	it('2. should call getServerData and update scope.myData when scope.getData is called', function(){
		var fakeResult = ['one', 'two', 'three'];
		spyOn(myHttpSvc, 'getServerData').andCallFake(function(){
			return {
				then: function(callback){ callback(fakeResult); return this; },
				finally: function(callback){ return callback(this); }
			};
		});
		//// understand why we need to return the object 'this' in 'then' part.
		//// as you can see that the same object is needed to call 'finally' (in the controller)
		
		//// in the controller, if we had the async function called without finally,
		//// we would have set the spy as per below 
		//spyOn(myHttpSvc, 'getServerData').andCallFake(function(){
		//	return {
		//		then: function(callback){ return callback(fakeResult); }
		//	};
		//});

		scope.getData();
		scope.$digest();
		expect(myHttpSvc.getServerData).toHaveBeenCalled();
		expect(angular.equals(scope.myData, fakeResult)).toBe(true);
	});

	it('3. should call getData when view is loaded', function(){
		var fakeResult = ['one', 'two', 'three'];
		spyOn(scope, 'getData');
		spyOn(myHttpSvc, 'getServerData').andCallFake(function(){
			return {
				then: function(callback){ callback(fakeResult); return this; },
				finally: function(callback){ return callback(this); }
			};
		});

		var viewTemplate = '<div ng-controller="myHttpController"></div>';
		$compile(viewTemplate)(scope);
		scope.$digest();
		expect(scope.getData);
	});

	it('4. should do logging when name property gets changed - $watch', function(){
		expect(scope.name).toBe('');
		spyOn(logMock, 'log');

		scope.name = 'Updated name';
		scope.$digest();

		expect(logMock.log).toHaveBeenCalledWith('Updated name');

	});

});
describe('testing myMathSvc', function(){

	var svcInstance;

	beforeEach(function(){

		module('mySvcApp');

		// do injection in the argument of this 'function'
		inject(function($injector){
			svcInstance = $injector.get('myMathSvc');
		});
	});

	it('1. should add two values', function(){
		var x = 5, y = 7;
		var result = svcInstance.add(x, y);
		expect(result).toEqual(x+y);
	});

	it('2. should throw error when any of the operands isNaN', function(){
		var x = 5, y = 'FOO';
		
		expect(function(){ svcInstance.add(x, y)}).toThrow(new Error('invalid parameters'));
	});

	describe('testing divide method', function(){

		it('3. should throw error if operators are isNaN', function(){

			//ref: https://stackoverflow.com/a/27876020
			expect(function(){ svcInstance.divide('foo', 'bar')}).toThrow(new Error('invalid parameters'));

			expect(function(){ svcInstance.divide('foo', 5)}).toThrow(new Error('invalid parameters'));

			expect(function(){ svcInstance.divide(5, 'bar')}).toThrow(new Error('invalid parameters'));
		});

		it('4. should throw error if divisor is zero', function(){
			expect(function(){ svcInstance.divide(5, 0)}).toThrow(new Error('divide by zero error'));
		});


		it('5. should give me proper result when both operands are proper', function(){
			var op1 = 10, op2 = 2;

			var result = svcInstance.divide(op1, op2);
			expect(result).toEqual(5);
		});
	});

});
describe('testing myThisController', function(){

	var $controller, $timeout, scope;
	var ctrl, getController;

	beforeEach(function(){
		module('mySvcApp');
		module('myApp');

		inject(function($injector){
			$controller = $injector.get('$controller');
			scope = $injector.get('$rootScope').$new();
			myMathSvc = $injector.get('myMathSvc');
			$timeout = $injector.get('$timeout');

			getController = function(){
				return $controller('myThisController', {
					'$scope': scope,
					'myMathSvc': myMathSvc,
					'$timeout': $timeout
				});
			};

			ctrl = getController();
		});

	});

	it('1. should load controller properly', function(){
		expect(ctrl).not.toBeNull();
		expect(ctrl).not.toBe(undefined);
		
		expect(ctrl.param1).toBe(0);
		expect(ctrl.param2).toBe(0);
	});

	it('2. should call math service to add operands user has provided', function(){
		var op1 = 5, op2 = 2, fakeResult = 7;
		spyOn(myMathSvc, 'add').andReturn(fakeResult);

		ctrl.param1 = op1;
		ctrl.param2 = op2;
		ctrl.addition();
		scope.$digest();

		expect(myMathSvc.add).toHaveBeenCalled();
		expect(myMathSvc.add).toHaveBeenCalledWith(ctrl.param1, ctrl.param2);
		expect(ctrl.result).toBe(fakeResult);
	});

	/*it('3. testing timeout', function(){
		var op1 = 5, op2 = 2, fakeResult = 7;
		spyOn(myMathSvc, 'add').andReturn(fakeResult);

		ctrl.param1 = op1;
		ctrl.param2 = op2;
		ctrl.delayedAddition();
		$timeout.flush();
		scope.$digest();

		expect(myMathSvc.add).toHaveBeenCalled();
		expect(myMathSvc.add).toHaveBeenCalledWith(ctrl.param1, ctrl.param2);
		expect(ctrl.result).toBe(fakeResult);

		$timeout.verifyNoPendingTasks();
	});*/
});
var myApp = angular.module('myApp');

myApp.controller('myHttpController',
	['$scope', 'myHttpSvc', '$log',
	function($scope, myHttpSvc, $log){

		// add plunk in the blog - as inline plunk in the blog
		$scope.myData = [];
		$scope.showLoader = false;
		$scope.name = '';

		$scope.getData = function(){
			$scope.showLoader = true;

			myHttpSvc.getServerData().then(function(result){
				$scope.myData = result;
			}, errorCallback).finally(function(){
				$scope.showLoader = false;
			});
		};

		$scope.addData = function(){
			$scope.showLoader = true;

			myHttpSvc.addData($scope.name).then(function(result){
				if(result == true){
					$scope.name = '';
					$scope.getData();
				} else{
					errorCallback(result);
				}
			}, errorCallback).finally(function(){
				$scope.showLoader = false;
			});
		};

		$scope.$on('$viewContentLoaded', function(){
			$scope.getData();
		});

		$scope.$watch('name', function(newVal, oldVal){
			$log.log(newVal);
		})

		function errorCallback(errorData){
			$log.log(errorData);			
		}
	}]
);