angularjs unit testing unable to inject a service - angularjs

I have a really simple service:
'use strict';
angular.module('sapphire.orders').service('deliveryDatesService', service);
function service() {
return {
clearAddressReason: clearAddressReason,
getMinDate: getMinDate
};
//////////////////////////////////////////////////
function clearAddressReason(model, dateReasons, reasons) {
if (model.manualAddress) {
model.overrideDates = true;
reasons.date = dateReasons.filter(function (item) {
return item.value === 'D4';
})[0];
} else {
reasons.address = null;
if (!reasons.date || reasons.date.value === 'D4') {
reasons.date = null;
model.overrideDates = false;
}
}
};
function getMinDate(model) {
var now = new Date();
// If we are not overriding dates, set to today
if (!model.overrideDates) return now;
// If dates are overriden, then the min date is today + daysToDispatch
return new Date(now.setDate(now.getDate() + model.daysToDispatch));
};
};
It has no dependencies, so I want to test the methods.
So I have tried to create a spec like this:
'use strict';
describe('Service: deliveryDatesService', function () {
beforeEach(module('sapphire.orders'));
var service,
reasons,
dateReasons;
beforeEach(inject(function (deliveryDatesService) {
console.log(deliveryDatesService);
service = deliveryDatesService;
reasons = {};
dateReasons = [{ value: 'D4' }];
}));
it('can create an instance of the service', function () {
expect(service).toBeDefined();
});
it('if manual delivery address is true, then override dates should be true', function () {
var model = { manualDeliveryDate: true };
service.clearAddressReason(model, dateReasons, reasons);
expect(model.overrideDates).toBe(true);
});
it('if manual delivery address is false, then override dates should be false', function () {
var model = { manualDeliveryDate: false };
service.clearAddressReason(model, dateReasons, reasons);
expect(model.overrideDates).toBe(false);
});
it('minimum date cannot be less than today', function () {
var model = { };
var minDate = service.getMinDate(model);
var now = new Date();
expect(minDate).toBeGreaterThan(now);
});
});
But my service is always undefined. Can someone tell me what I am doing wrong please?
Update
So, it turns out this is to do with one or more services interfering somehow.
In my karma.conf.js I had declared all my bower applications and then this:
'src/app/app.module.js',
'src/app/**/*module.js',
'src/app/**/*constants.js',
'src/app/**/*service.js',
'src/app/**/*routes.js',
'src/app/**/*.js',
'test/spec/**/*.js'
I created a test service in the root of my scripts directory and then created a spec file to see if it was created. It moaned at me about a reference error in a file that was not related at all. It moaned about this bit of code:
angular.module('sapphire.core').factory('options', service);
function service($rootScope) {
return {
get: get,
save: save
};
//////////////////////////////////////////////////
function get() {
if (Modernizr.localstorage) {
var storageData = angular.fromJson(localStorage.options);
if (storageData) {
return angular.fromJson(storageData);
}
}
return {
background: {
enabled: true,
enableSnow: true,
opacity: 0.6
}
};
};
function save(options) {
if (Modernizr.localstorage) {
localStorage.options = angular.toJson(options);
$rootScope.$options = get();
}
};
};
stating that Modernizr is not defined.
I changed the code to this:
angular.module('sapphire.core').factory('options', service);
function service($rootScope) {
return {
get: get,
save: save
};
//////////////////////////////////////////////////
function get() {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
var storageData = angular.fromJson(localStorage.options);
if (storageData) {
return angular.fromJson(storageData);
}
}
return {
background: {
enabled: true,
enableSnow: true,
opacity: 0.6
}
};
};
function save(options) {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
localStorage.options = angular.toJson(options);
$rootScope.$options = get();
}
};
};
and it started working. But my other test was not.
So I changed my references in karma.conf.js to this:
'src/app/app.module.js',
'src/app/orders/orders.module.js',
'src/app/orders/shared/*.js',
'test/spec/**/*.js'
and it started working.
That leads me to believe there is something wrong with my application somewhere. Maybe another reference like Modernizr. I still have an outstanding question though. How can services that are not dependant on another service interfere?
I think it's worth noting that each service, controller, directive is in it's own file and they all follow this structure:
(function () {
'use strict';
angular.module('sapphire.core').factory('options', service);
function service($rootScope) {
return {
get: get,
save: save
};
//////////////////////////////////////////////////
function get() {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
var storageData = angular.fromJson(localStorage.options);
if (storageData) {
return angular.fromJson(storageData);
}
}
return {
background: {
enabled: true,
enableSnow: true,
opacity: 0.6
}
};
};
function save(options) {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
localStorage.options = angular.toJson(options);
$rootScope.$options = get();
}
};
};
})();
I am wondering that because I wrap them in anonymous functions that execute themselves, is that what is causing this problem?
* Solution *
So in the end I found out exactly what was causing this issue. It was indeed to do with the file in karma.conf.js. I had told it to load all files and somewhere in there was something it didn't like.
After a bit of playing I finally found what it was and thought I would share it just in case someone else gets here.
The issue was routes. I am using ui.router and it appears that having them in your tests fail.
I changed my files section to this:
'src/app/app.module.js',
'src/app/**/*module.js',
'src/app/**/*constants.js',
'src/app/**/*service.js',
'src/app/**/*controller.js',
//'src/app/**/*routes.js',
'test/spec/**/*.js'
As you can see I have a routes file(s) commented out. If I bring them back in, everything fails.

I think your inner deliveryDatesService variable is hiding the external one.
To avoid that, you can try puting underscores around the inner service variable as per the spec on the website below:
https://docs.angularjs.org/api/ngMock/function/angular.mock.inject
Look for
'Resolving References (Underscore Wrapping)'
on that page.
Then your code would look like this:
beforeEach(inject(function (_deliveryDatesService_) {
console.log(_deliveryDatesService_);
service = _deliveryDatesService_;
reasons = {};
dateReasons = [{ value: 'D4' }];
}));
Also you need to make sure that all required files and directories are declared in karma.conf.js so that the test framework can use them.

Related

How to mock $window.Notification

I am still learning the ropes when it comes to unit testing with angular. I have an angular service that I use to create HTML5 notifications. Code is similar to the following:
(function() {
'use strict';
angular
.module('blah')
.factory('OffPageNotification', offPageNotificationFactory);
function offPageNotificationFactory($window) {
//Request permission for HTML5 notifications as soon as we can
if($window.Notification && $window.Notification.permission !== 'denied') {
$window.Notification.requestPermission(function (status) { });
}
function OffPageNotification () {
var self = Object.create(OffPageNotification.prototype);
self.visibleNotification = null;
return self;
}
OffPageNotification.prototype.startNotification = function (options) {
var self = this;
self.options = options;
if(self.options.showHtml5Notification && (!self.options.onlyShowIfPageIsHidden || $window.document.hidden)) {
if($window.Notification && $window.Notification.permission !== 'denied') {
self.visibleNotification = new $window.Notification('Notification', {
body: self.options.notificationText,
icon: self.options.notificationIcon
});
}
}
};
.
.
.
return new OffPageNotification();
}
})();
I am attempting to write unit tests for this but am unsure how to mock $window.Notification so it can be used as both a constructor...
self.visibleNotification = new $window.Notification(....)
and also contain properties
if($window.Notification && $window.Notification.permission !== 'denied')
and methods....
$window.Notification.requestPermission(
An example of something I have tried is:
describe('startNotification', function() {
beforeEach(function() {
var mockNotification = function (title, options) {
this.title = title;
this.options = options;
this.requestPermission = sinon.stub();
};
mockNotification.prototype.permission = 'granted';
mockWindow = {
Notification: new mockNotification('blah', {}),
document: {hidden: true}
};
inject(function (_OffPageNotification_) {
OffPageNotification = _OffPageNotification_;
});
});
it('should display a html5 notification if the relevant value is true in the options, and permission has been granted', function(){
var options = {
showHtml5Notification: true,
onlyShowIfPageIsHidden: true
};
OffPageNotification.startNotification(options);
});
});
I get an error saying '$window.Notification is not a constructor' with this setup and I understand why (I am passing in an instantiated version of the mockNotification). But if I set mockWindow.Notification = mockNotification then I get an error when it calls requestPermission since this is undefined.
Any help is appreciated
Notification should be a constructor. And it should have static properties and methods.
All of the relevant properties of mockNotification are instance properties, while they should be static:
function MockNotification() {}
MockNotification.title = title;
MockNotification.options = options;
MockNotification.requestPermission = sinon.stub();
mockWindow = {
Notification: MockNotification,
document: {hidden: true}
};

Angular string binding not working with ControllerAs vm

I've been trying to do a two-way bind to a string variable on the Controller. When the controller changes the string, it isn't updated right away. I have already run the debugger on it and I know that the variable vm.overlay.file is changed. But it isn't updated on the View... it only updates the next time the user clicks the button that fires the selectOverlayFile() and then it presents the previous value of vm.overlay.file
Here goes the code:
(function () {
angular
.module("myapp.settings")
.controller("SettingsController", SettingsController);
SettingsController.$inject = [];
function SettingsController() {
var vm = this;
vm.overlay = {
file: undefined,
options: {
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
destinationType: Camera.DestinationType.DATA_URL
}
};
vm.errorMessages = [];
vm.selectOverlayFile = selectOverlayFile;
vm.appMode = "photo";
vm.appModes = ["gif-HD", "gif-video", "photo"];
activate();
function activate() {
}
function selectOverlayFile() {
navigator.camera.getPicture(successOverlay, errorOverlay, vm.overlay.options);
}
function successOverlay(imageUrl) {
//If user has successfully selected a file
vm.overlay.file = "data:image/jpeg;base64," + imageUrl;
}
function errorOverlay(message) {
//If user couldn't select a file
vm.errorMessages.push(message);
}
}
})();
Thanks!
After a couple of hours searching for the issue and testing various solutions. I finally found it. The issue was that when the navigator.camera.getPicture(successOverlay, errorOverlay, vm.overlay.options) calls the callback function, it is out of AngularJS scope. So we need to notify Angular to update binding from within these callbacks using $scope.$apply():
(function () {
angular
.module("myapp.settings")
.controller("SettingsController", SettingsController);
SettingsController.$inject = ["$scope"];
function SettingsController($scope) {
var vm = this;
vm.overlay = {
file: undefined,
options: {
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
destinationType: Camera.DestinationType.DATA_URL
}
};
vm.errorMessages = [];
vm.selectOverlayFile = selectOverlayFile;
vm.appMode = "photo";
vm.appModes = ["gif-HD", "gif-video", "photo"];
activate();
///////////////////
function activate() {
}
function selectOverlayFile() {
navigator.camera.getPicture(successOverlay, errorOverlay, vm.overlay.options);
}
function successOverlay(imageUrl) {
//If user has successfully selected a file
vm.overlay.file = "data:image/jpeg;base64," + imageUrl;
$scope.$apply();
}
function errorOverlay(message) {
//If user couldn't select a file
vm.errorMessages.push(message);
$scope.$apply();
}
}
})();

Sharing a service through filter between two controllers doesn't work in angular 1.3.x

OK guys Im pretty sure its documented somewhere but I cannot seem to locate it, this is why I apologize in advance if its already been discussed.
Im trying to share a localization service through a filter across different controllers/directives etc.
In angular 1.2.x it was working, but in 1.3.x it doesnt.
See plunkr
Uncomment the script to switch between 1.2.x/1.3.x
var app = angular.module('plunker', []);
app.service('trnsService', ['$rootScope',
function($rootScope) {
var trnsService = {},
trns = {
'CONSTANT': {
'en': 'En text',
'bla': 'Bla text'
}
},
lan = 'en';
trnsService.setLang = function setLang(lang) {
lan = lang;
console.log(lan);
if (!$rootScope.$$phase) {
$rootScope.$apply();
}
};
trnsService.getTrns = function getTrns(key) {
return trns[key][lan]
};
return trnsService;
}
])
.filter('trns', ['trnsService',
function(trnsService) {
return function(input) {
return trnsService.getTrns(input);
};
}
])
.controller('MainCtrl', function($scope, trnsService) {
$scope.setLang = function setLang(lg) {
trnsService.setLang(lg);
}
})
.controller('SecCtrl', function($scope, trnsService) {
$scope.setLang = function setLang(lg) {
trnsService.setLang(lg);
}
});
Found it. Setting the filter as $stateful did it for me. Just wish I found it earlier and havent lost entire day on it...
.filter('trns', ['trnsService',
function(trnsService) {
// return function(input) {
// return trnsService.getTrns(input);
// };
function decorateFilter(input) {
return trnsService.getTrns(input);
}
decorateFilter.$stateful = true;
return decorateFilter;
}
])
plunkr
Some more info here:
https://github.com/angular-translate/angular-translate/issues/720
and here
https://docs.angularjs.org/guide/filter

AngularJS v1.3 breaks translations filter

In Angular v1.2 I was using the following code for serving up localised strings in the application:
var i18n = angular.module('i18n', []);
i18n.service('i18n', function ($http, $timeout) {
/**
A dictionary of translations keyed on culture
*/
this.translations = {},
/**
The current culture
*/
this.currentCulture = null,
/**
Sets the current culture, loading the associated translations file if not already loaded
*/
this.setCurrentCulture = function (culture) {
var self = this;
if (self.translations[culture]) {
$timeout(function () {
self.currentCulture = culture;
});
} else {
$http({ method: 'GET', url: 'i18n/' + culture + '/translations.json?' + Date.now() })
.success(function (data) {
// $timeout is used here to defer the $scope update to the next $digest cycle
$timeout(function () {
self.translations[culture] = data;
self.currentCulture = culture;
});
});
}
};
this.getTranslation = function (key) {
if (this.currentCulture) {
return this.translations[this.currentCulture][key] || key;
} else {
return key;
}
},
// Initialize the default culture
this.setCurrentCulture(config.defaultCulture);
});
i18n.filter('i18n', function (i18n) {
return function (key) {
return i18n.getTranslation(key);
};
});
In the template it is then used as follows:
<p>{{ 'HelloWorld' | i18n }}</p>
For some reason that I can't fathom, upgrading to v1.3 of AngularJS has broken this functionality. Either the $timeout isn't triggering a digest cycle, or the filter isn't updating. I can see that the $timeout code is running, but the filter code never gets hit.
Any ideas why this might be broken in v1.3?
Thanks!
In angular 1.3 the filtering was changed so that they are no longer "stateful". You can see more info in this question: What is stateful filtering in AngularJS?
The end result is that filter will no longer re-evaluate unless the input changes. To fix this you can add the line:
i18n.filter('i18n', function (i18n) {
var filter = function (key) {
return i18n.getTranslation(key);
};
filter.$stateful = true; ///add this line
return filter;
});
Or else implement your filter some other way.

AngularJS - self referencing services?

I'm building an Angular app that will have a top-level Controller and a second-level controller. There will be n number of second-level controllers, but I want to put global-level functions someplace. I'm doing this in a service.
I'm starting down the path of creating a single service that return an api, really, containing lots of functions (below). The service is returning an object with two property branches that each contain a set of functions. How can I call one of these from the other?
globalModule.factory('global', function($http) {
var squares = MyApp.squares; // this is the *only* link from Global namespace to this app
return {
squareMgr: {
getSquaresEarned: function() {
return squares.earned;
},
getSquaresPlaced: function() {
return squares.placed;
},
setThisSquareEarned: function(value) {
squares.earned.push(value);
},
setThisSquarePlaced: function(value) {
squares.placed.push(value);
}
},
missionMgr: {
missionInfo: {},
setMissionInfo: function(missionInfo) {
this.missionInfo = missionInfo
},
complete: function(missionData) {
log('complete called on video at ' + new Date());
missionData.complete = true;
log(angular.toJson(missionData));
$http({
url: '/show/completeMission',
method: "POST",
data: missionData
})
.then(function(response) {
if (response.data.success === true) {
log('completeMission success');
// increment squares earned counter
this.squareMgr.setThisSquareEarned(missionData.id);
// above is an attempt to run a function contained in this
// same service in a different parent property branch.
// how *should* I do this?
}
});
}
}
}
});
How about something like this:
globalModule.factory('global', function($http) {
var glob = {
squareMgr: {
// ...
},
missionMgr: {
foo: function() {
glob.squareMgr.xyz();
}
}
};
return glob;
});

Resources