AngularJS karma testing with chained promises - javascript

Here is the function in the Controller:
angular.module("MyMod")
.controller("MyController", function(UserData, mtcLogService, $state,
BroadcastService, $scope, $rootScope, ChartsService, PaxCountSummaryService) {
self.waveCountSummary = function() {
if (self.program.programID !== undefined) {
PaxCountSummaryService.getWaveCountChartSummary(self.program.programID)
.then(function(data) {
self.totalWaveCount = data[data.length - 1].count;
data.pop();
return (data || []).map(_.bind(ChartsService.tasksCountToChartData, this, _, "unknown"));
})
.then(function(chart) {
return self.replaceContentsOf(self.waveCountChartData, chart);
});
}
};
});
Here is my working test:
fit("My test", inject(function ($rootScope, $controller, $q) {
var results = [{
pop: sinon.spy(),
count: 1
}];
mockPaxCountSummaryService.getWaveCountChartSummary
.returns($q.resolve(results));
testController = $controller("PaxCountSummaryController", {
$state: state,
$scope: scope,
$rootScope: $rootScope,
PaxCountSummaryService: mockPaxCountSummaryService
});
testController.program = testProgram;
testController.totalWaveCount = null;
expect(testController.totalWaveCount).toBe(null);
testController.waveCountSummary();
scope.$apply();
expect(testController.totalWaveCount).toBe(1);
}));
Ok, this test works. But how would I get to the second THEN in the promise chain so I can test if REPLACECONTENTSOF is called?
Any help on this?

Well, this is one of those times where I got turned around with multiple issues but it does work as expected. I just needed to create a spy for REPLACECONTENTSOF.
Here is working final test:
fit("MyTest.", inject(function($controller, $q) {
var results = [{
pop: sinon.spy(),
count: 1
}];
mockPaxCountSummaryService.getWaveCountChartSummary
.returns($q.resolve(results));
testController = $controller("PaxCountSummaryController", {
$state: state,
$scope: scope,
PaxCountSummaryService: mockPaxCountSummaryService
});
testController.program = testProgram;
testController.replaceContentsOf = sinon.spy();
testController.totalWaveCount = null;
expect(testController.replaceContentsOf.callCount).toBe(0);
testController.waveCountSummary();
scope.$apply();
expect(testController.totalWaveCount).toBe(1);
expect(testController.replaceContentsOf.callCount).toBe(1);
}));

Related

Unit testing angularJS controller where data is updated using angular.copy from a factory after a GET call

I'm really new to Angular Unit testing using Karma and Jasmine, and I'm unable to find anything that would guide me in the right direction with regards to my problem. I have my Angular app called myApp. The app has multiple components each defined as their own modules that are then injected in the app.
App
angular.module('myApp', ['ngRoute', 'ui-bootstrap', 'dashboard', 'projects']);
Controller
angular.module('dashboard', []).controller('dashboardController',
['dashboardService', '$rootScope', '$scope', '$http', function() {
$scope.teamMemebrs = dashboardService.teamMembers;
dashboardService.getTeamMemebrs();
}]);
Service
angular.module('dashboard').service('dashboardService',
['$http', '$q', function($http, $q) {
let loadTeamMembers = () => {
return $http.get(APIendPoint).then( (response) => {
return response;
}, (response) => {
return response;
} );
};
let dashboardService = {
teamMembers : [],
getTeamMembers() {
return $q.when(loadTeamMembers()).then( (response) => {
let team = response.status === 200 ? response.data : [];
angular.copy(team, dashboardService.teamMemebrs);
});
}
};
return dashboardService;
}
]);
I am trying to test the controller and my test looks as follows, but the test fails because scope.teamMembers is undefined. What am I doing wrong?
Test Spec
describe('Dashboard', () => {
let scope, dashboardController, dashboardService, $httpBackend;
//This is what API call is expected to return
let sampleResponse = [ {name: 'Jane Doe'},
{name: 'Amy Smith'},
{name: 'John Hopkins'} ];
beforeEach(angular.mock.module('myApp'));
beforeEach(angular.mock.module('dashboard'));
beforeEach(angular.mock.inject( (_$rootScope_, _$controller_, _dashboardService_, _$httpBackend_) => {
scope = _$rootScope_.$new();
$controller = _$controller_;
dashboardService = _dashboardService_;
spyOn(dashboardService, 'getTeamMembers').and.callThrough();
dashboardController = $controller('dashboardController', { $scope: scope, dashboardService: dashboardService });
scope.$apply();
}));
it('should exist', () => {
expect(dashboardController).toBeDefined();
});
it('call dashboardService and populate scope.teamMembers', () => {
expect(dashboardService.getTeamMembers).toHaveBeenCalled();
expect(scope.teamMemebrs).toEqual(sampleResponse);
});
});

AngularJS/Karma/Jasmine - Service call not returning value

I'm attempting to make a call to a Github API using a service injected into a component - and yes, I am using AngularJS 1.5.3.
In the unit test, I am not receiving back a value (the function does work when I run it in the browser). I'm not sure what I'm doing wrong and hopefully someone could point me in the right direction.
Here's the error:
main.component.js
(function(){
angular.module("app").component("mainComponent", {
templateUrl: "/templates/main.component.html",
controllerAs: "vm",
controller: function(APIFactory, UserFactory, $state){
const vm = this;
vm.searchGithub = function(){
APIFactory.getAPI(vm.searchText).then(function(res){
res.status !== 200 ? $state.go("404", {errorData: res.data }) : (
vm.User = new UserFactory.User(res.data),
$state.go("profile", {userData: vm.User})
);
})
.catch(function(err){
$state.go("fourOFour");
});
};
}
});
})();
main.component.spec.js
describe("Main Component", function(){
var mainComponent, APIFactory, UserFactory, $httpBackend, $q, $state, $rootScope;
const addy = "https://api.github.com/users/";
beforeEach(angular.mock.module("app"));
beforeEach(inject(function(_APIFactory_, _UserFactory_, _$httpBackend_, _$state_, _$q_, _$rootScope_, _$componentController_){
APIFactory = _APIFactory_;
UserFactory = _UserFactory_;
$httpBackend = _$httpBackend_;
$state = _$state_;
$q = _$q_;
$rootScope = _$rootScope_;
$rootScope.$new();
mainComponent = _$componentController_("mainComponent", { $scope : {} });
}));
describe("Checking if the searchGithub() worked correctly", function(){
var result;
beforeEach(function(){
spyOn(mainComponent, "searchGithub").and.callThrough();
spyOn(APIFactory, "getAPI").and.callThrough();
result = {};
});
it("should make a call to UserFactory", function(){
mainComponent.searchText = "someName";
expect(mainComponent.searchText).toBeDefined();
// RESPONSE_SUCCESS does exist, I've omitted it.
$httpBackend.whenGET(addy + mainComponent.searchText).respond(200, $q.when(RESPONSE_SUCCESS));
// This is where I expect something to work
APIFactory.getAPI(mainComponent.searchText).then(function(res){
result = res;
});
$httpBackend.flush();
expect(APIFactory.getAPI).toHaveBeenCalledWith(mainComponent.searchText);
expect(mainComponent.User).toBeDefined();
});
});
});
So this is what I came up with for a solution. If anybody wants to give me a better solution, I am up for ideas.
First I made two mocks and then I injected them into mainComponent, as well as a spy for my mocked APIFactoryMock.getAPI function:
const APIFactoryMock = {
getAPI: function(){}
};
const UserFactoryMock = {
User: function(data){
return {
login: data.login,
id: data.id,
avatar_url: data.avatar_url,
html_url: data.html_url,
followers: data.followers,
following: data.following,
public_repos: data.public_repos,
public_gists: data.public_gists,
created_at: data.created_at,
updated_at: data.updated_at,
name: data.name,
company: data.company,
blog: data.blog,
location: data.location,
bio: data.bio,
hireable: data.hireable,
email: data.email,
links: {
followers_url: data.followers_url,
following_url: data.following_url,
subscriptions_url: data.subscriptions_url,
repos_url: data.repos_url,
organizations_url: data.organizations_url
}
}
}
};
beforeEach(inject(function(_APIFactory_, _UserFactory_, _$httpBackend_, _$state_, _$q_, _$rootScope_, _$componentController_){
APIFactory = _APIFactory_;
UserFactory = _UserFactory_;
$httpBackend = _$httpBackend_;
$state = _$state_;
$q = _$q_;
$rootScope = _$rootScope_;
$rootScope.$new();
spyOn(APIFactoryMock, "getAPI").and.returnValue(RESPONSE_SUCCESS);
bindings = { APIFactory: APIFactoryMock, UserFactory: UserFactoryMock, $state: $state };
mainComponent = _$componentController_("mainComponent", { $scope : {} }, bindings);
}));
And then I wrote tests for the mocks:
it("should make a call to UserFactory", function(){
mainComponent.searchText = "someName";
expect(mainComponent.searchText).toBeDefined();
mainComponent.searchGithub(mainComponent.searchText);
$httpBackend.whenGET(addy + mainComponent.searchText).respond(200, $q.when(RESPONSE_SUCCESS));
$httpBackend.flush();
mainComponent.User = UserFactoryMock.User(RESPONSE_SUCCESS.data);
expect(mainComponent.searchGithub).toHaveBeenCalledWith(mainComponent.searchText);
expect(mainComponent.User).toBeDefined();
expect(mainComponent.User.id).toEqual(666);
});
In the above answer, you are manually making a call to UserFactoryMock.User in the testcase, which will create an user object.
But to correctly test the functionality, to should be checking for UserFactory.User to be called when call to APIFactory.getAPI is success (without making a call to UserFactory.User manually in the testcase.
I would suggest modifying your testcase to something like below:
describe("Main Component", function(){
var mainComponent, APIFactory, UserFactory, $httpBackend, $q, $state, $rootScope;
const addy = "https://api.github.com/users/";
beforeEach(angular.mock.module("app"));
beforeEach(inject(function(_APIFactory_, _UserFactory_, _$httpBackend_, _$state_, _$q_, _$rootScope_, _$componentController_){
APIFactory = _APIFactory_;
UserFactory = _UserFactory_;
$httpBackend = _$httpBackend_;
$state = _$state_;
$q = _$q_;
$rootScope = _$rootScope_;
var scope = $rootScope.$new();
var bindings = { APIFactory: APIFactory, UserFactory: UserFactory, $state: $state };
mainComponent = _$componentController_("mainComponent", { $scope : scope }, bindings);
}));
describe("Checking if the searchGithub() worked correctly", function(){
var result;
beforeEach(function(){
spyOn(mainComponent, "searchGithub").and.callThrough();
spyOn(APIFactory, "getAPI").and.callFake(function() {
var def = $q.defer();
def.resolve(RESPONSE_SUCCESS);
return def.promise;
});
spyOn(UserFactory, "User").and.callFake(function() {
var user = { id: 666, .... };
return user;
});
});
it("should make a call to UserFactory", function(){
mainComponent.searchText = "someName";
$rootScope.$apply();
expect(mainComponent.searchText).toBeDefined();
mainComponent.searchGithub(); // Call the same way as it works in the code actually.
$rootScope.$apply();
//No manual call to 'UserFactory.User' or 'APIFactory.getAPI'. The call to 'APIFactory.getAPI' is resolved/succeeds, hence a call to 'UserFactory.User' is made and the same is tested
expect(APIFactory.getAPI).toHaveBeenCalledWith(mainComponent.searchText);
expect(UserFactory.User).toHaveBeenCalledWith(RESPONSE_SUCCESS.data);
expect(mainComponent.User).toBeDefined();
expect(mainComponent.User.id).toEqual(666);
});
});
});

Not a function (mocking error?)

I'm trying to do some unit testing on my Ionic app, using the Karma-jasmine framework, and it's giving me quite the headache. I have a function in my controller I want to test by giving it some dummy input (the credentials), but I keep getting the errors
TypeError: 'undefined' is not a function (evaluating '$controller')
TypeError: 'undefined' is not an object (evaluating '$scope.credentials.username = "uname"')
I've tried some alternate ways to do this, but one of them kept giving me some bogus that I needed to add dependencies from my services.js as well (i.e. $ionicPopup), which might have some truth to it, but I'm super new to all of this. Any help would be greatly appreciated.
I've made a lot of progress by mostly rewriting this, but I'm now getting the error
TypeError: 'undefined' is not a function (evaluating 'LoginService.initiateRequest(
$scope.credentials.username,
$scope.credentials.password)')
at c:/Users/name/git/mobile/www/js/controllers.js:18
It's saying that it's not a function in my controllers.js file...which it DEFINITELY is. Really not understanding this one. Is it something to do with services?
I have here my controllers.tests.js file...
describe('Controllers', function() {
beforeEach(module('myApp.services', 'myApp.controllers'));
var mockRequest = {
}
beforeEach(function() {
module(function($provide) {
$provide.value('initiateRequest', mockRequest);
});
});
var $rootScope, $controller, $sharedProperties, $resource;
beforeEach(inject(function(_$controller_, $rootScope) {
myScope = $rootScope.$new();
$scope = {};
$state = {};
LoginService = {};
localStorageService = {};
sharedProperties = {};
$ionicLoading = myScope;
initiateRequest = mockRequest;
$controller = _$controller_;
}));
describe('$scope.login()', function() {
it('does something', function() {
var $scope = {};
var controller = $controller('LoginController', {
$scope: $scope,
$state: $state,
LoginService: LoginService,
localStorageService: localStorageService,
sharedProperties: sharedProperties,
$ionicLoading: $ionicLoading
});
expect($scope.login).toBeDefined(true);
expect($scope.show).toBeDefined(true);
$scope.login();
});
});
});
describe('Controllers', function () {
beforeEach(module('myApp.services', 'myApp.controllers'));
var mockRequest = {};
beforeEach(function () {
module(function ($provide) {
$provide.value('initiateRequest', mockRequest);
});
});
var $controller, sharedProperties, state, scope, ionicLoading, LoginService, localStorageService, initiateRequest;
beforeEach(function () {
inject(function ($rootScope, _$controller_) {
scope = $rootScope.$new();
$controller = _$controller_;
ionicLoading = {};
sharedProperties = {};
LoginService = {};
localStorageService = {};
initiateRequest = mockRequest;
});
});
describe('$scope.login()', function () {
beforeEach(function () {
$controller('LoginController', {
$scope: scope,
$state: state,
LoginService: LoginService,
localStorageService: localStorageService,
sharedProperties: sharedProperties,
$ionicLoading: ionicLoading
});
scope.$apply();
});
it('does something', function () {
expect(scope.login).toBeDefined();
expect(scope.show).toBeDefined();
});
});
});

AngularJS $scope not accessible outside the function

I've written the following code to get the value from input fields and save it.
I defined a global variable and put the output in it to use in app.factory. the problem is "x" is only readable inside the "update function" and undefined anywhere outside it.
how can I solve this?
var app = angular.module('bomApp', ['bomGraph']);
app.controller('bomController', ['$scope', 'appService', '$rootScope', function ($scope, appService, $rootScope) {
var get = function () {
appService.get().then(function (promise) {
$scope.graph = {
options: {
"hierarchicalLayout": {
"direction": "UD"
},
"edges": {
"style":"arrow-center",
"color":"#c1c1c1"
},
"nodes": {
"shape":"oval",
"color":"#ccc"
}
},
data: {
nodes: promise.nodes,
edges: promise.edges
}
};
});
};
$scope.newNode = {
id: undefined,
label: undefined,
level: undefined,
parent: undefined,
};
$scope.arrNode = {};
$scope.update = function (nodes) {
$scope.arrNode = angular.copy(nodes);
$rootScope.x = angular.copy(nodes);
};
$scope.newEdge = {
id: undefined,
from: undefined,
to: undefined
};
$scope.arrEdge = {};
$scope.updateE = function (edges) {
$scope.arrEdge = angular.copy(edges);
};
get();
}]);
app.factory('appService', ['$q', '$http', '$rootScope', function ($q, $http, $rootScope) {
console.log($rootScope.x);
return {
get: function (method, url) {
var deferred = $q.defer();
$http.get('data.json')
.success(function (response) {
deferred.resolve(response);
})
return deferred.promise;
},
};
}]);
var x; is local to the controller alone. It can't be accessed in the factory.
Mostly if you want to share data between controllers you will store it in service. It's a bad practice also to share declare global variables in the controller
In your case, actually a closure is being created and x is a private variable. It can be accessed only within the controller.
In case you want to access variable x in service, then use $rootScope
example:
app.controller('bomController', ['$scope', 'appService', '$rootScope',function ($scope, appService,$rootScope) {
$scope.update = function (nodes) {
$scope.arrNode = angular.copy(nodes);
$rootScope.x = angular.copy(nodes);
};
});
In your service:
app.factory('appService', ['$q', '$http', '$rootScope', function ($q, $http, $rootScope) {
// you will have access to $rootScope.x
});
You can create a global $scope variable instead, $scope.x;
Declare x outside of the function:fiddle
<div ng-app="app">
<div ng-controller="ctrl">
<button ng-click="fn()">{{x}} click</button>
</div>
</div>
JS
angular.module("app", [])
.controller("ctrl", function ($scope, $rootScope) {
$scope.x = "hello";
$scope.fn = function () {
$scope.x = "world";
}
})

AngularJS scope not updating after data changes in factory method

I feel like I'm missing something obvious here, but I'm still stymied.
I update Thing on the scope by calling the create function on the ThingFactory. But when I reference the scope from PromoteController, the scope still contains the old version of Thing (with ID of 1).
This seems like a place where I'd want to use $scope.$apply(), but that causes the 'digest already in progress' error.
What am I missing?
var app = angular.module('app', ['ngRoute']);
app.factory('ThingFactory', ['$http', '$q', '$routeParams', function ($http, $q, $routeParams) {
var deferred = $q.defer();
return {
get: function(id) {
var thing = {
id: 393,
name: 'Can I be gotten?',
description: 'get'
};
deferred.resolve(thing);
return deferred.promise;
},
save: function (thing) {
console.log("ThingFactory -> CREATE");
var thing = {
id: 122,
name: 'after create.',
description: 'creatine'
};
deferred.resolve(thing);
return deferred.promise;
},
init: function() {
console.log("ThingFactory -> INIT");
var thing = {
id: 1,
name: 'initial value',
description: 'INIT'
};
deferred.resolve(thing);
return deferred.promise;
}
};
}]);
app.config(function ($routeProvider, $locationProvider, $httpProvider) {
$locationProvider.html5Mode(true);
$routeProvider
.when('/build', {
templateUrl: '/build.html',
controller: 'BuildController'
})
.when('/things/:id/promote', {
templateUrl: '/promote.html',
controller: 'PromoteController'
})
});
app.controller('BuildController', function ($scope, $http, $location, ThingFactory) {
// HERE I INITIALIZE THE THING
ThingFactory.init().then(function(thing) {
$scope.thing = thing;
});
$scope.saveNewThing = function() {
// HERE I 'SAVE' THE THING
ThingFactory.save($scope.thing).then(function(thing) {
$scope.thing = thing;
$location.path("/" + thing.id + "/promote");
})
}
});
app.controller('PromoteController', function ($scope, $http, $routeParams, ThingFactory) {
// HERE'S WHERE THE THING ON THE SCOPE SHOULD HAVE AN ID OF 122,
// BUT IS STILL 1
var id = $routeParams.id;
ThingFactory.get({id: id}).then(function(thing) {
$scope.thing = thing;
});
});
please create a var deferred = $q.defer(); for every method in you factory. otherwise you always use the same deferred and this is resolved with the value in your init function.

Categories