This is a controller with a submit function:
$scope.submit = function(){
$http.post('/api/project', $scope.project)
.success(function(data, status){
$modalInstance.dismiss(true);
})
.error(function(data){
console.log(data);
})
}
}
This is my test
it('should make a post to /api/project on submit and close the modal on success', function() {
scope.submit();
$httpBackend.expectPOST('/api/project').respond(200, 'test');
$httpBackend.flush();
expect(modalInstance.dismiss).toHaveBeenCalledWith(true);
});
The error I get is:
Error: Unexpected request: GET views/appBar.html
views/appBar.html is my templateUrl:
.state('project', {
url: '/',
templateUrl:'views/appBar.html',
controller: 'ProjectsCtrl'
})
So somehow ui-router is making my $httpBackend point to this instead of my submit function. I have the same issue in all my tests using $httpBackend.
Is there any solution to this?
Take this gist
https://gist.github.com/wilsonwc/8358542
angular.module('stateMock',[]);
angular.module('stateMock').service("$state", function($q){
this.expectedTransitions = [];
this.transitionTo = function(stateName){
if(this.expectedTransitions.length > 0){
var expectedState = this.expectedTransitions.shift();
if(expectedState !== stateName){
throw Error("Expected transition to state: " + expectedState + " but transitioned to " + stateName );
}
}else{
throw Error("No more transitions were expected! Tried to transition to "+ stateName );
}
console.log("Mock transition to: " + stateName);
var deferred = $q.defer();
var promise = deferred.promise;
deferred.resolve();
return promise;
}
this.go = this.transitionTo;
this.expectTransitionTo = function(stateName){
this.expectedTransitions.push(stateName);
}
this.ensureAllTransitionsHappened = function(){
if(this.expectedTransitions.length > 0){
throw Error("Not all transitions happened!");
}
}
});
Add it to a file called stateMock in your test/mock folder, include that file in your karma config if it isn't already picked up.
The setup before your test should then look something like this:
beforeEach(module('stateMock'));
// Initialize the controller and a mock scope
beforeEach(inject(function ($state //other vars as needed) {
state = $state;
//initialize other stuff
}
Then in your test you should add
state.expectTransitionTo('project');
This Github issue about Unit Testing UI Router explains more fully what's happening.
The problem is that $httpBackend.flush() triggers a broadcast which then triggers the otherwise case of the stateProvider.
A simple solution can be to do the following setup, as mentionned by #darinclark in Github thread mentionned above. This is valid if you do not need to test state transitions. Otherwise have a look to #rosswil's answer that is inspired by #Vratislav answer on Github.
beforeEach(module(function ($urlRouterProvider) {
$urlRouterProvider.otherwise(function(){return false;});
}));
EDITED
Thanks to Chris T to report this in the comments, seems after v0.2.14? the best way to do this is to use
beforeEach(module(function($urlRouterProvider) {
$urlRouterProvider.deferIntercept();
}));
If you don't want to add gist files like it says in the correct solution you can add a "when" condition to your $httpBackend to ignore GET petitions of views like this:
$httpBackend.when("GET", function (url) {
// This condition works for my needs, but maybe you need to improve it
return url.indexOf(".tpl.html") !== -1;
}).passThrough();
I have the same error your commented, after a call service they ask me about the url of otherwise ui-route.
To solve the problem of call the otherwise ui-route in testing is not inject $state in beforeach statment. In my testing $state not have sense to use it.
Move your services to their own module that have no dependency on ui.router. have your main app depend on this module. When you test don’t test the main app, test the module that has your services in it. The stateprovider won’t try to change state/route because this module knows nothing about the ui.router. This worked for me.
Related
Following is my myService.spec.js :
'use strict';
describe('myService', function () {
var dependentService,dependentService1,rootScope,$q;
beforeEach(module('myModule.myConfig'));
beforeEach(module('myModule'));
beforeEach(inject(function (_myService_, _$rootScope_,
_$q_,_dependentService1_,_dependentService_) {
myService= _myService_;
rootScope = _$rootScope_.$new();
$q = _$q_;
dependentService1= _dependentService1_;
dependentService= _dependentService_;
spyOn(dependentService1,'setPath');
spyOn(dependentService,'get');
spyOn($q,'all').and.callFake(function(){
var deferred = _$q_.defer();
if($q.all.calls.count() === 1){
deferred.resolve([{path:'./abcd'}]);
}else if($q.all.calls.count() === 2){
deferred.resolve([{path:'./abcd','user': {'userId':'xyz',
'groups':['G1']}}]);
}else{
deferred.resolve({});
}
return deferred.promise;
});
}));
it('should load path, information',function(){
var promise = myService.load();
rootScope.$apply();
expect(dependentService.get).toHaveBeenCalled();
expect(dependentService1.setPath).toHaveBeenCalledWith('./abcd');
});
});
And here is my MyService.js
'use strict';
function myService($q,dependentService1,dependentService){
var appConfigLoaded = false;
function _loadPath(){
return dependentService.get(dependentService1.url);
}
return {
load : function(){
var loadPath = _loadPath(),
finalDeferred = $q.defer();
$q.all([loadPath ]).then(function (results) {
var path = results[0].path ,
user = results[0].user;
dependentService1.setPath(path);
$q.all([_loadDataPromise1(),_loadDataPromise2()]).then(function(results){
finalDeferred.resolve(results);
},function(reason){
finalDeferred.reject(reason);
});
},function(reason){
finalDeferred.reject(reason);
});
return finalDeferred.promise;
}
};
}
angular.module('myModule.myConfig').service('myService', MyService);
Following functions and service which holds them are omitted for brevity but they return promise. I have spied them as well just like I have spied other two services.
loadDataPromise1() and loadDataPromise1()
Now I get some error like Unexpected request GET with URL which points to some headers.template.html. But I am not even making any call to http to get such template nor any of the functions do. which ever call $http.get I have spied them.
I tried with
$httpBackend.flush();
but the same error occur. May be I am doing something basic thing in a wrong way.
If I remove $rootScope.apply() then error goes away. However .then() function in my service attached to first call to $q.all() is not called.
Any pointers to help me out?
Do you have a default route with a templateURL in your application routing? You are probably running into this issue:
https://github.com/angular/angular.js/issues/2717
The workaround (which is annoying) is to put an expectGET for the templateURL in your beforeEach and then flush it.
$httpBackend.expectGET('path/to/template/defaulttemplate.html').respond(200, '');
$httpBackend.flush();
You can do it anywhere in there - I try and keep it at the top or bottom so it is obvious that is a workaround and not part of the testing code. Note you will have to put those lines in EVERY test file since the routing is part of app (not the module under test).
I've been trying to get my angular js page to work with indexeddb, and I'm trying to do it right. So far it's going smoothly but I've really been struggling getting my promises to work as I expect in regards to my data loading. I followed the advice of this other question and I think I understand what it's trying to do, but I can't get it to work. I think the issue is that it is using the routeProvider which expects ajax requests and I'm not doing that, it's all client side. I am using the angular-indexedDB pluging that can be found on GitHub here. These are the relevant bits of what I'm doing.
angular.module('characterApp',['ngRoute','xc.indexedDB'])
.constant('dbName', 'character')
.constant('storeName', 'character')
.constant('version', 1)
.constant('emptyCharacter', {})
.value('jsPlumbInstance', {})
.config(function($indexedDBProvider, dbName, storeName, version) {
$indexedDBProvider.connection(dbName)
.upgradeDatabase(version, function(event, db, tx){
db.createObjectStore(storeName, {keyPath: 'guid'});
});
})
.config(function($routeProvider){
console.log('Configuring route');
$routeProvider
.when('/js/angular/controllers/characterController.js', {
controller:'characterController',
resolve:{
'characterData':function(DataService){
console.log('resolving promise');
return DataService.promise;
}
}
})
})
.service('DataService', ['$indexedDB', 'storeName', 'emptyCharacter', function($indexedDB, storeName, newObject){
var objects = [];
var index = 0;
var objectStore = $indexedDB.objectStore(storeName);
var promise = objectStore.getAll().then(function(results) {
objects = results;
console.log("DB Objects loaded.");
});
console.log("Promise created");
function getControllerObject(propertyName){
return objects;
}
return {
getControllerObject : getControllerObject,
promise : promise
};
}])
.controller('characterController', ['$scope', 'DataService', function($scope, DataService) {
console.log('Promise is now resolved: ' + DataService.getControllerObject()
);
}]);
When I run it my console outputs the following:
Configuring route
characterApp.js:52 Promise created
characterController.js:2 Promise is now resolved:
characterApp.js:50 DB Objects loaded.
However if I understand the other answer mentioned above, the output should be:
Configuring route
characterApp.js:52 Promise created
characterApp.js:50 DB Objects loaded.
characterController.js:2 Promise is now resolved:
If it helps, my full code is on GitHub here, but you will need node.js to run the custom server.js file I have in the /www folder for all the content to load properly. You could get it to work with minimal effort elsewhere if you moved the content from the www/pages directory into their placeholders on the index.html though. Or infact, you could remove the nonstandard tags alltogether, I think it would still work. I suspect all of that is unnecessary and I just don't understand how these things work. I'm fairly new to angular but trying to learn how to do things the right way.
angular.module('characterApp',['ngRoute','xc.indexedDB'])
.constant('dbName', 'character')
.constant('storeName', 'character')
.constant('version', 1)
.constant('emptyCharacter', {})
.config(function($indexedDBProvider, dbName, storeName, version) {
$indexedDBProvider.connection(dbName)
.upgradeDatabase(version, function(event, db, tx){
db.createObjectStore(storeName, {keyPath: 'guid'});
});
})
.service('DataService', ['$indexedDB', 'storeName', 'emptyCharacter', function($indexedDB, storeName, newObject){
var objects = [];
var index = 0;
var objectStore = $indexedDB.objectStore(storeName);
var promise = objectStore.getAll()
console.log("Promise created");
function getControllerObject(propertyName){
return objects;
}
return {
getControllerObject : getControllerObject,
promise : promise
};
}])
.controller('characterController', ['$scope', 'DataService', function($scope, DataService) {
var characters = [];
DataService.promise.then(function(results){
characters = results;
$scope.character = characters[0];
console.log("DB Objects loaded.");
});
);
}]);
I didn't need the routeprovider at all, I just was doing stupid things with my promises.
I am modelling my routing based on this post: angular ui-router login authentication.
I am making a cordova phonegap app. I'm new to angularjs and it's quite a new learning experience so I appreciate any suggestions at all! If I'm building the authentication routing wrong please let me know.
I have 2 factories and a run method. My first factory is titled 'principal', and the most important variable is isFBLoggedIn. It also has has a return of 3 functions, 2 of which are initializers, one initializing quickblox and one initializing facebook. The 3rd function just resolves and returns the promise.
The second factory is named 'authorization' and really just redirects based on whether the variable isFBLoggedIn is true. It is resolved in my state named 'app' which is an abstract parent state. (It has a menu template and menu controller but not sure if that matters).
The run method simply sets the toState and toStateParams and then also routes based on isFBLoggedIn. I'm not sure if I needed to put routes here but I assumed I did since I believe factories only run once...
So my variable isFBLoggedIn is coming up undefined and I cant figure out why. I've initialized it in my code...
I tried to copy as much of the code from that link because I'm not very familiar with angularjs ui. However if you believe there is a better way that I should be authenticating please let me know. I'd love to do things properly the first time and not the 10th time...
I've been sitting/looking at this too long so I'm hoping I can get some help...What am I doing wrong?
Edit to add: In addition to the isFBLoggedIn variable showing up undefined, when I type the url into my browser for a page requiring login, I see the url in the browser changing to /login (which is what I want/expect). However the actual view does not change. I came across this link which had it seems the same issue. However I don't understand what they are saying is the problem...
run method
.run(['$rootScope','$state','$stateParams','principal','authorization',function($rootScope,$state,$stateParams,principal,authorization){
$rootScope.$on('$stateChangeStart',function(event,toState,toParams,fromState,fromParams){
$rootScope.toState = toState;
$rootScope.toStateParams = toParams;
// if logged in and going to login page, redirect to cards page
if (principal.isFBLoggedIn && toState.name === 'app.login'){
$state.go('app.cards');
} else if (!principal.isFBLoggedIn && toState.name != 'app.login') {
// not logged in and not going to login page, redirect to login page.
$state.go('app.login');
} else {
// this is theoretically all fine but that's not true
// because things are being routed here because isFBLoggedIn is showing undefined.
}
});
}])
principal factory
.factory('principal',['$q','$timeout',function($q,$timeout){
isFBLoggedIn = false;
return {
QBInit: function(){
QB.init(CONFIG.application_id, CONFIG.authKey, CONFIG.authSecret, CONFIG.debug);
QB.createSession(function(err, result) {
if (err){
return $q.reject("QB Create Session Error");
} else {
return result.token;
}
});
},
FBInit: function(){
if (!window.cordova) {
// first check if fb is defined and run browser init and get login status if successful
checkFBDefined(function(){
facebookConnectPlugin.browserInit(1506810102869030,"v2.0");
facebookConnectPlugin.getLoginStatus(
function(res){
if (typeof(res) != 'undefined' && res.status === 'connected'){
this.isFBLoggedIn = true;
}
return res;
},
function(err){return $q.reject('getLoginStatus Error: ' + err);
});
},0,$timeout);
} else {
facebookConnectPlugin.getLoginStatus(
function(res){
if (typeof(res) != 'undefined' && res.status === 'connected'){
this.isFBLoggedIn = true;
}
return res;
},
function(err){return $q.reject('getLoginStatus Error: ' + err);
});
}
},
initialization: function(){
var deferred = $q.defer();
var promise = deferred.promise.then(this.QBInit).then(this.FBInit);
deferred.resolve();
return promise;
}
}
}])
authorization factory
.factory('authorization',['$rootScope','$state','principal',function($rootScope,$state,principal){
return {
authorize: function(){
return principal.initialization().then(function(){
if (principal.isFBLoggedIn && $rootScope.toState.name == 'app.login'){
// logged in, but going to login page, redirect to cards page
$state.go('app.cards');
} else if (!principal.isFBLoggedIn && $rootScope.toState.name !='app.login'){
// not logged in and not going to login page, redirect to login page.
$rootScope.returnToState = $rootScope.toState;
$rootScope.returnToStateParams = $rootScope.toStateParams;
$state.go('app.login');
} else {
console.log("routes should be fine but isn't because isFBLoggedIn is undefined");
}
});
}
}
}])
where authorization factory is resolved:
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app', {
url: "/app",
abstract: true,
templateUrl: "templates/menu.html",
resolve:{authorize: ['authorization',function(authorization){
return authorization.authorize();
}]
},
controller: 'MenuCtrl'
})
You init isFBLoggedIn = false; as global variable and it is not bound to factory principal you inject to app.run.
Try this:
.factory('principal',['$q','$timeout',function($q,$timeout){
var isFBLoggedIn = false;
return {
isFBLoggedIn: isFBLoggedIn,
QBInit: function(){ // rest of code
when you initialize isFBLoggedIn you have:
isFBLoggedIn = false;
I belive you want
this.isFBLoggedIn = false;
I have a resource factory that builds objects for accessing our API. I use an environment variable to determine the base part of the URL - whether or not to include 'account/id' path segments when the admin user is 'engaging' a client account.
The sessionStorage item that holds the 'engagedAsId' doesn't get read, though for instances created after engaging an account. It requires a full reload of the app to pick up that change. Here is the factory code:
myapp.factory('ResourceSvcFactory',
['$rootScope', '$resource',
function ($rootScope, $resource) {
function ResourceSvcFactory (endpoint) {
// vvv problem is here vvv
var accountId = sessionStorage.getItem('engagedAsId');
var apiPath = (accountId != null)
? '/api/account/' + accountId + endpoint
: '/api' + endpoint;
var Resource = $resource(apiPath+':id/',{
// default params
id:''
},{
// custom actions
update: {method: 'PUT'}
});
return Resource;
}
return ResourceSvcFactory;
}]);
myapp.factory('AssetsResource', ['ResourceSvcFactory', function (ResourceSvcFactory) {
var endpoint = '/assets/';
var Resource = ResourceSvcFactory(endpoint);
return Resource;
}]);
I implement this in my Controller like this:
myapp.controller('AssetGroupListCtrl', [ 'AssetgroupsResource', function (AssetgroupsResource) {
var AssetGroups = AssetgroupsResource;
// ... rest of controller
}]);
When i run this it works fine. But, if i change the engaged status in the sessionStorage without a full reload, the instance in the controller does not pick up the new path.
Is there a way to 'refresh' the instance? ...automatically?
After hours of research, it appears that the fundamental flaw in what I'm trying to do in the question is this: I'm trying to use a 'singleton' as a 'class'. from the docs:
Note: All services in Angular are singletons. That means that the injector uses each recipe at most once to create the object. The injector then caches the reference for all future needs.
http://docs.angularjs.org/guide/providers
My work around was to create the $resource inside a method of a returned object. Here is an example:
MyApp.factory('AssetgroupsResource',
['$rootScope', '$resource',
function ($rootScope, $resource) {
return {
init: function () {
var accountId = sessionStorage.getItem('engagedAsId');
var apiPath = (accountId != null)
? '/api/account/' + accountId + endpoint
: '/api' + endpoint;
// default params
id:''
},{
// custom actions
});
return Resource;
}
}
}]);
This made it possible to build the object at the right time in the controller:
MyApp.controller('AssetGroupListCtrl', ['Assetgroups', function (Assetgroups) {
var Assetgroups = AssetgroupsResource.init();
// now I can use angular's $resource interface
}]);
Hope this helps someone. (or you'll tell me how this all could've been done in 3 lines!)
You can always call $scope.$apply(); to force an angular tick.
See a nice tutorial here: http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
I think $resource uses promise which might be an issue depending on how you implement your factory in your controller.
$scope.$apply() can return an error if misused. A better way to make sure angular ticks is $rootScope.$$phase || $rootScope.$apply();.
When I use ng-include as a title, how do I catch the error when the address (file path) does not exist?
I finished a ng-include router inside a ng-view(with ng-route),
It's a little bit like this:
ContentCtrl:
var content = $route.current.params.content,
tmplArr = content.split("_"),
tmpl = {},
personId=$route.current.params.personId||$scope.persons[0].id;
$scope.personId=personId;
tmpl.url = "content/";
for (var i = 0, len = tmplArr.length; i < len; i++) {
tmpl.url += tmplArr[i] + "/";
}
tmpl.url = tmpl.url.substring(0, tmpl.url.length - 1) + ".html";
$scope.template = tmpl;
ContentView:
<div ng-include="template.url" class="ng-animate"></div>
when I use the addr is not exist like:/home/#/content/profile_asdfa,
the angular just fetch the resource in a loop.
So I need to catch the ng-include error,when there is no template file in the hash.
Can anybody Help me ? Thx!
Looking in the source for ngInclude, there seems to be no hook or way to detect directly a 404 (or other) error when the template doesn't exist. You might want to consider a feature request to add this, as it sounds like a useful feature.
However, right now you could do something with a http response interceptor. If there is some way to tell if a http reguest is for a template, say it is in the 'content' directory, you can intercept errors, and do something with them. For example you could replace the data with a custom directive, that then emits an event so controller(s) could respond to it.
The interceptor could be written like:
app.config(function ($httpProvider) {
$httpProvider.interceptors.push('templateInterceptor');
});
// register the interceptor as a service
app.factory('templateInterceptor', function($q) {
return {
'responseError': function(rejection) {
var isTemplate = !!rejection.config.url.match(/^content/g);
if (isTemplate) {
rejection.data = '<div><template-error url="\''+ (rejection.config.url) + '\'"><strong>Error from interceptor.</strong></template-error></div>';
return rejection;
} else {
return $q.reject(rejection);
}
}
}
});
So when there is an error after fetching something from the 'content' directive, it adds an element <template-error> in place of the template content. When this is compiled and then linked, it $emits a custom event, templateError, which parent controllers can respond to, by $scope.$on. So the directive can be code up like:
app.directive('templateError', function() {
return {
restrict: 'E',
scope: {
'url': '='
},
link: function(scope) {
scope.$emit('templateError', {url:scope.url});
}
};
});
And then in the parent controller of the original ngInclude, you can react to this event:
$scope.$on('templateError', function(e, data) {
$scope.templateError = true;
$scope.templateErrorUrl = data.url;
})
You can see the full working code in this Plunker. Although I think this is slightly hacky, if the Angular team decide to add an $emited event to the code of ngInclude on error, then it should be easy to just remove the interceptor / your custom element.