AngularJS - How to limit service visibility? - javascript

I'm trying to create an angular service which will be visible only in some modules (but not in all). I have tried to create a service in a module and then include it in a different module, but the service is still visible in the entire application.
Example:
var myAppSubModule = angular.module('myAppSubModule',[]);
myAppSubModule.factory('TestSubService', function() {
return {
testFnc: function() {
return 'test';
}
}
});
var myAppModule = angular.module('myAppModule',['myAppSubModule']);
myAppModule.factory('TestService', function(TestSubService) {
return {
testFnc: function() {
return TestSubService.testFnc(); //this should work
}
}
});
var myApp = angular.module('myApp',['myAppModule']);
function MyCtrl($scope, TestSubService, TestService) {
$scope.name = TestService.testFnc(); //this should work
$scope.name2 = TestSubService.testFnc(); //this should fail
}
I want TestSubService to be visible in TestService, but not in the entire application.
Is there any way to achieve this?

Related

must return a value from $get factory method

I am trying to test an Angular service which I know works functionally as its been in place for a while. I've just come to retro fit some tests around it now but I am getting some strange issue where I get a message "must return a value from $get factory method" from the jasmine test.
The service does work in the website and the service returns an API
var service = {
setStartLanguage: setStartLanguage,
setLanguage: setLanguage,
supportedLanguages: supportedLanguages,
currentLanguage: etCurrentLanguage.currentLanguage,
getLanguage: getLanguage
};
return service;
I believe this is enough to get it working but I can't figure out why the test is complaining about it not returning a $get.
Oh and all I'm doing for the test is trying to expect that the service is defined.
EDIT:
Full service code,
(function () {
'use strict';
angular.module('app.core').factory('etLanguageService', ['$translate', '$route', 'etCurrentLanguage', 'supportedLanguages', 'localStorageService', etLanguageService]);
function etLanguageService($translate, $route, etCurrentLanguage, supportedLanguages, localStorageService) {
function setLanugageFromLocalStorage(storedLanguage) {
etCurrentLanguage.currentLanguage = storedLanguage;
$translate.preferredLanguage(etCurrentLanguage.currentLanguage.code);
localStorageService.set('currentLanguage', etCurrentLanguage.currentLanguage);
}
function setLanguageFromBrowser() {
var language = etCurrentLanguage.get();
if (!language) {
language = '';
}
var cleanLanguage = language.substr(0, 2).toLowerCase();
var supportedLanguage = _.find(supportedLanguages, function(language) {
return language.code === cleanLanguage;
});
$translate.preferredLanguage(supportedLanguage.code);
localStorageService.set('currentLanguage', supportedLanguage);
}
function setStartLanguage() {
// first check if we have a stored language
var storedLanguage = localStorageService.get('currentLanguage');
if (storedLanguage) {
setLanugageFromLocalStorage(storedLanguage);
} else {
setLanguageFromBrowser();
}
}
function setLanguage(language) {
if (etCurrentLanguage.currentLanguage !== language) {
etCurrentLanguage.currentLanguage = language;
$translate.use(etCurrentLanguage.currentLanguage.code).then(function () {
localStorageService.set('currentLanguage', etCurrentLanguage.currentLanguage);
$route.reload();
});
}
}
function getLanguage() {
return localStorageService.get('currentLanguage');
}
var service = {
setStartLanguage: setStartLanguage,
setLanguage: setLanguage,
supportedLanguages: supportedLanguages,
currentLanguage: etCurrentLanguage.currentLanguage,
getLanguage: getLanguage
};
return service;
}
}());
That's hard to tell without more test code.
$get is the method from the provider of your service. For each service, there is a provider which is responsible of creating an instance of your service through the $get method. If you have a service called myService, then there is a provider myServiceProvider. Internaly, when bootstraping your app, Angular calls mysServiceProvider.$get to get an instance of your service.
If you're tinkering with providers and mocks of providers, that could be a reason...

how to check a function in an injected controller?

i'm currently just trying to test if getTodaysHours function on my controller has been called. ultimately the function should get hours from the mock JSON data and pass if parameters match, but i'm stuck on the first part.
vendor.controller
export class VendorController {
constructor($rootScope, data, event, toastr, moment, _, distanceService, vendorDataService, userDataService, stateManagerService) {
'ngInject';
//deps
this.$rootScope = $rootScope;
this.toastr = toastr;
this._ = _;
this.userDataService = userDataService;
this.vendorDataService = vendorDataService;
this.stateManagerService = stateManagerService;
this.event = event;
//bootstrap
data.isDeepLink = true;
this.data = data;
this.data.last_update = moment(this.data.updated_at).format('MM/DD/YY h:mm A');
this.data.distance = distanceService.getDistance(this.data.loc.lng, this.data.loc.lat);
this.data.todaysHours = this.getTodaysHours();
this.data.rating_num = Math.floor(data.rating);
this.hasReviewed = (userDataService.user.reviewed[data._id]) ? true : false;
this.isGrid = false;
this.isSearching = false;
this.hideIntro = true;
this.menuCollapsed = true;
this.filterMenuCollapsed = true;
this.selectedCategory = 'All';
this.todaysHours = '';
this.type = '';
this.searchString = '';
this.reviewScore = 0;
this.today = new Date().getDay();
this.vendorDataService.currentVendor = data;
//load marker onto map
$rootScope.$broadcast(event.ui.vendor.pageLoad, data);
//get menu
vendorDataService.getVendorMenu(data._id)
.then((res)=> {
this.data.menu = res.menu;
this.menuContainer = this.data.menu;
this.totalResults = this.getTotalResults();
this.availableMenuCategories = this.getAvailableMenuCategories();
})
.catch(() => {
this.toastr.error('Whoops, Something went wrong! We were not able to load the menu.', 'Error');
});
}
//get todays hours
getTodaysHours() {
let today = this.data.hours[new Date().getDay()];
return (today.opening_time || '9:00am') + ' - ' + (today.closing_time || '5:00pm');
}
}
the first test passes when I mock the JSON data with $provide constant
describe('vendor controller', () => {
let vm,
data = {"_id":"56b54f9368e685ca04aa0b87","lat_lon":"33.713018,-117.841101","hours":[{"day_of_the_week":"sun","closing_time":" 7:00pm","opening_time":"11:00am","day_order":0,"id":48880},...];
beforeEach(angular.mock.module('thcmaps-ui', ($provide) => {
$provide.constant('data', new data);
}));
//first test
it('should pass', () => {
expect(data._id).toEqual('56b54f9368e685ca04aa0b87');
});
//second test
it('should call getTodaysHours', () => {
expect(vm.getTodaysHours()).toHaveBeenCalled();
});
});
then I tried to inject the controller (not sure if correct syntax):
beforeEach(angular.mock.module('thcmaps-ui', ($provide) => {
$provide.constant('data', new data);
}));
beforeEach(inject(($controller) => {
vm = $controller('VendorController');
spyOn(vm,'getTodaysHours').and.callThrough();
}));
and it gives me some kind of forEach error. the second test gives me a undefined error when evaluating vm.getTodaysHours():
PhantomJS 2.1.1 (Mac OS X 0.0.0) vendor controller should pass FAILED
forEach#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:341:24
loadModules#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4456:12
createInjector#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4381:22
workFn#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular-mocks/angular-mocks.js:2507:60
/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4496:53
PhantomJS 2.1.1 (Mac OS X 0.0.0) vendor controller should call getTodaysHours FAILED
forEach#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:341:24
loadModules#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4456:12
createInjector#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4381:22
workFn#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular-mocks/angular-mocks.js:2507:60
/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4496:53
TypeError: undefined is not an object (evaluating 'vm.getTodaysHours') in /Users/adminuser/Documents/workspace/thcmaps-ui/.tmp/serve/app/index.module.js (line 9)
/Users/adminuser/Documents/workspace/thcmaps-ui/.tmp/serve/app/index.module.js:9:244419
You need to inject the dependencies of your controller when instantiating it with $controller. For example, consider the following controller:
class MyController {
constructor($rootScope, $log) {
// Store the controllers dependencies
this.$rootScope = $rootScope;
this.$log = $log;
}
// Return obituary value from the $rootScope
getValue() {
this.$log.debug('Retrieving value');
return this.$rootScope.foobar;
}
// Get the current date
getDate() {
this.$log.debug('Getting date');
return Date.now()
}
static get $inject() {
return ['$scope', '$log'];
}
}
I've written this controller using ES6, note that the dependencies are defined within the static $injectgetter at the foot of the class declaration. This will be picked up by AngularJS upon instantiation.
As you can see, the controller depends upon the $rootScope and the $log provider. When mocking this controller for testing purposes, you must inject the controllers dependencies like this:
describe('Spec: MyController', () => {
var controller;
beforeEach(inject(($rootScope, $log, $controller) => {
controller = $controller('MyController', {
$rootScope,
$log
});
});
it('should return a value from the $rootScope', () => {
var value = controller.getValue();
// ... perform checks
});
it('should return the current date', () => {
var date = controller.getDate();
// ... perform checks
});
});
More recent versions of Jasmine enable developers to leverage the this keyword throughout their tests.
Any beforeEach, afterEach, and it declarations will all share the same reference to this, allowing you to avoid creating enclosed variables (like var controller, as seen above) and also avoid creating unnecessary globals. For example:
beforeEach(inject(function ($rootScope, $log, $controller) {
this.controller = $controller('MyController', {
$rootScope,
$log
});
});
it('should return a value from the $rootScope', function () {
this.value = controller.getValue();
// ... perform checks
});
Note the second argument in the call to $controller, this must be an object containing the expected dependencies that your controller ('MyController', in this case) relies upon.
The reasoning behind this is simply to allow developers to pass mock services, factories, providers, etc to the controller as an alternative to spies.
$controller: https://docs.angularjs.org/guide/unit-testing
this: http://jasmine.github.io/2.0/introduction.html
Apologies for the unuseful link to the Jasmine documentation regarding the usage of this with tests, I couldn't add a direct link to the correct section of the page due to how their anchor tags are set out (the anchor tag contains a <code></code> block, doh!).

How to test function within Angular click-handler

I'm currently unit-testing my Angular controllers and the only portion of my code coverage that is lagging behind are functions within click-handlers, and the statements within these functions.
As an example, function(cap)... states function not covered and playersService.setCap... states statement not covered in relation to the below click-handler:
vm.setCap = function(cap) {
playersService.setCap({playerId: playerId}, {limit: cap});
};
How would I go about testing a function like this, and the statement within it? I'm just looking for a basic test (expect(function).toHaveBeenCalled).
Alright to test this you would want to use a mock version of your playersService which you can then just inject into your controller.
describe("Controller: yourController", function () {
var mockResponse = {};
var mockService = {
setCap: function(playerId, limit){
mockResponse.playerId = playerId,
mockResponse.limit = limit
}
};
var mockParams = 'cap';
var $controller;
beforeEach(inject(function (_$controller_) {
$controller = _$controller_;
}))
it("Should call the service on click", function () {
spyOn(mockService, 'setCap').and.callThrough();
var testedController = $controller('yourController', { playersService:mockService });
testedController.setCap(mockParams);
expect(mockService.toHaveBeenCalled).toHaveBeenCalled();
expect(mockResponse.limit.limit).toBe(mockParams);
})
});
This will give you an example for both white and blackbox testing for the functionality.

Loading data inside config block in AngularJS

What would be the right (angular-wise) way to load data in the app.config block?
Would a boot module+provider help? i tried the following approach (streamlined) http://jsfiddle.net/Vd5Pg/1/ with no success.
angular.module('boot',[]).provider('test', function(){
this.$get = function() {
return {
getString: function() { return "i am a string"; }
}
};
});
angular.module('main',['boot']).config(['testProvider', function(test) {
//no luck with getString
}]);
The real life situation is that i'd like to load localized routes and determine current language before configuring the $routeProvider.
TIA.
Your current code returns a test service with the method getString. In order to getString() within the provider you should do the below.
angular.module('boot',[]).provider('test', function(){
// Method available in testProvider
this.getString = function () {
return "i am a string";
}
this.$get = function() {
return {
// Your test service.
}
};
});
angular.module('main',['boot']).config(['testProvider', function(test) {
var myString = testProvider.getString();
}]);

How to properly overwrite the exceptionHandler in angularjs?

For an app I'm using a skeleton that is very similar to https://github.com/angular/angular-seed.
I've tried something like this, in services.js:
'use strict';
/* Services */
angular.module('mApp.services', []).
factory('$exceptionHandler', function () {
return function (exception, cause) {
alert(exception.message);
}
});
This doesn't do anything, it doesn't seem to overwrite the exceptionHandler.
If I try:
var mod = angular.module('mApp', []);
mod.factory('$exceptionHandler', function() {
return function(exception, cause) {
alert(exception);
};
});
It overwrite the whole app.
How can I properly overwrite the exceptionHandler if I am using a skeleton similar to the default angular app?
It's hard to know for certain without seeing the rest of your app, but I'm guessing angular.module('myApp').factory( ... will work. If you leave out the second parameter (,[]) angular will retrieve an existing module for further configuring. If you keep it angular will create a new module.
try this example
http://jsfiddle.net/STEVER/PYpdM/
var myApp = angular.module('myApp', ['ng']).provider({
$exceptionHandler: function(){
var handler = function(exception, cause) {
alert(exception);
//I need rootScope here
};
this.$get = function() {
return handler;
};
}
});
myApp.controller('MyCtrl', function($scope, $exceptionHandler) {
console.log($exceptionHandler);
throw "Fatal error";
});

Categories