I'm working on an AngularJS app that uses a custom directive. I'm also trying to use unit testing in my app. So, I'm trying to leverage Jasmine for that. Currently, My unit test is the problem. Currently, it looks like the following:
myDirective.spec.js
describe('Directive: myDirective', function () {
describe('Function: myProcedure', function () {
it('should word', function ($rootScope, $compile) {
var e = angular.element('<div><br /></div>');
console.log($compile);
$compile(e)($rootScope);
expect(true).toBe(true);
});
});
});
This test is throwing an exception. When the console.log line is ran, it prints: 'undefined'. The next line throws an error to Jasmine that says: 'TypeError: undefined is not a function'.
Its like I'm not injecting the $compile service. However, I believe I'm doing so. My test runner looks like the following:
<html ng-app="testApp">
<head>
<title></title>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.13/angular.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.0.0/jasmine.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.0.0/jasmine-html.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.0.0/boot.js"></script>
<script type="text/javascript" src="index.html.js"></script>
<script type="text/javascript" src="directives/myDirective.js"></script>
<script type="text/javascript" src="tests/unit/myDirective.spec.js"></script>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.0.0/jasmine.css" />
</head>
<body>
run tests
</body>
</html>
Why cannot I not run this basic test? What am I doing wrong?
First you must add angular mocks:
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.13/angular-mocks.js">
</script>
then load the modules and inject $compile/$rootScope in beforeEach blocks:
describe('Directive: myDirective', function () {
var $compile;
var $rootScope;
beforeEach(module('testApp'));
beforeEach(inject(function(_$compile_, _$rootScope_){
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
describe('Function: myProcedure', function () {
it('should word', function () {
var e = angular.element('<div><br /></div>');
$compile(e)($rootScope);
expect(true).toBe(true);
});
});
});
Check unit testing docs: http://docs.angularjs.org/guide/dev_guide.unit-testing#directives
The it() function provided by jasmine won't do any injection for you, so you need to inject your angular components into your tests. Modify your test as follows:
describe('Directive: myDirective', function () {
describe('Function: myProcedure', function () {
it('should word', function () {
inject(function ($rootScope, $compile) {
var e = angular.element('<div><br /></div>');
//rest of code goes here
});
You also need a reference to the angular.mocks library.
I have come across this problem just now and this is what made my day (in the very end of it actually).just add "module('ng');" along with module('yourAppModule'). It should work.
Related
I get the following error when adding a directive to my site:
Error: [ng:areq] Argument 'MainController' is not a function, got undefined
The error only occurs when I include the welcome-directive (welcome.js) in my site. If the import is removed the controller works.
Body of my index.html
<body ng-app="my-app">
<div ng-controller="MainController">
<welcome></welcome>
</div>
<!-- Angular -->
<script src="bower_components/angular/angular.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers/mainController.js"></script>
<!-- Without this line the controller works -->
<script src="js/directives/welcome.js"></script>
</body>
app.js
(function() {
var app = angular.module('my-app', []);
})();
mainController.js
angular.module('my-app', [])
.controller('MainController', ['$scope', function($scope) {
console.log('Controller working');
}]);
welcome.js
angular.module('my-app', [])
.directive("welcome", function() {
return {
restrict: "E",
template: "<div>Test test.</div>"
};
});
You are redefining the module. Define module only once.
when used as angular.module('my-app', []) it defined the module this should be used only once. When you want retrieve it. then use angular.module('my-app')
Use
angular.module('my-app') //Notice remove []
.directive("welcome", function() {
});
passing a 2nd argument like this angular.module('my-app', []) creates a new module, use it only once.
To retrive a module use var module = angular.module('my-app') and use module.controller(... or module.directive(.... etc.,
I'm trying to inject $scope in a controller created using the $controller service.
I'm getting the following error:
Unknown provider: $scopeProvider <- $scope <- TestController
It's important to mention that the creation is happening inside a directive's link function.
I've written a simple example to show the error.
app.js:
(function() {
angular.module('app', [])
.controller('TestController', [
'$scope',
function($scope) {
// I want to be able to use 'message' from the directive's template
// as if the controller was loaded directly using ng-controller
$scope.message = 'Hello World!';
}
])
.directive('directive', [
'$controller',
function($controller) {
return {
restrict: 'A',
template: '<div>{{ message }}</div>',
link: function(scope) {
// In the actual app the controller is dynamically selected
// I'm registering a $watch here that provides me the
// name of the controller
scope.myController = $controller('TestController');
}
};
}
]);
})();
index.html:
<!DOCTYPE html>
<html ng-app="app">
<head>
<title>Testing injection</title>
</head>
<body>
<div directive></div>
<script src="angular.js"></script>
<script src="app.js"></script>
</body>
</html>
Question: Why is this happening? Can you explain me the logic behind this behaviour? Any workaround?
Thank you.
Following Digix's answer, I was able to make it work:
var locals = {};
locals.$scope = scope;
$controller('TestController', locals);
My assumption is that in this way, instead of creating a new scope, the controller shares the one of the directive.
var locals = {};
locals.$scope = scope.$new();
$controller('testCtrl', locals);
I'm to setup standalone jasmine testing. I believe I have everything setup correctly. When I run my first test:
describe('Logon Controller', function() {
var controller, $scope;
beforeEach(module("app"));
//beforeEach(inject(function ($rootScope, $controller) {
// scope = $rootScope.$new();
// contoller = $controller('logonCtrl', {
// $scope: scope
// });
//}));
beforeEach(inject(function ($rootScope) {
scope = $rootScope.$new();
}));
//it('should say hello', function ($controller, $rootScope) {
// //var scope = $rootScope.$new();
// var controller = $controller('logonCtrl', { $scope: $scope });
// //expect(angular.isFunction(scope.get)).toEqual("Hello There :)");
// expect(scope.Hello()).toEqual("Hello There :)");
// expect($scope.Hello).toBeDefined();
//});
it('should say hello', inject(function ($controller, $rootScope) {
//var scope = $rootScope.$new();
var controller = $controller('logonCtrl', {$scope: $scope});
//expect(angular.isFunction(scope.get)).toEqual("Hello There :)");
//expect(scope.Hello()).toEqual("Hello There :)");
expect($scope.Hello).toBeDefined();
}));
});
I get this error message: Error: [$injector:modulerr] http://errors.angularjs.org/1.3.11/$injector/modulerr?p0=app.............
Here is my app module config.
var app = angular.module('app', ['ui.router','ngMaterial']);
Here is my controller:
app.controller('logonCtrl', ['$scope', '$state', '$interval', function ($scope, $state, $interval) {
$scope.hello = function() {};
});
]);
I am using the standalone version of Jasmine 2.2.0. I have no clue what the issue could be, everything I have done is pretty basic stuff. Any help would be very much appreciated.
UPDATE: here is what my SpecRunner.html looks like:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.2.0</title>
<link rel="shortcut icon" type="image/png" href="JasmineUnitTest/lib/jasmine-2.2.0/jasmine_favicon.png">
<link rel="stylesheet" href="JasmineUnitTest/lib/jasmine-2.2.0/jasmine.css">
<script type="text/javascript" src="JasmineUnitTest/lib/jasmine-2.2.0/jasmine.js"></script>
<script type="text/javascript" src="JasmineUnitTest/lib/jasmine-2.2.0/jasmine-html.js"></script>
<script type="text/javascript" src="JasmineUnitTest/lib/jasmine-2.2.0/boot.js"></script>
<script type="text/javascript" src="JasmineUnitTest/lib/jasmine-2.2.0/angular.min.js"></script>
<script type="text/javascript" src="JasmineUnitTest/lib/jasmine-2.2.0/angular-mocks.js"></script>
<!-- include source files here... -->
<script type="text/javascript" src="js/controllers/logonCtrl.js"></script>
<!-- include spec files here... -->
<script type="text/javascript" src="JasmineUnitTest/specs/logonController-spec.js"></script>
</head>
<body>
</body>
</html>
It looks like the $injector fails to find some of your dependencies. Are you sure the script containing your controller is properly loaded ?
Is there any way to inject AngularJS's $log into every service and controller? It just feels a little redundant specifying it for every one.
Another way you could do it is to add a method to the rootScope, and then access it through $scope.$root in your controllers, thus avoiding another injection. I don't know if it is as bad as globals.
testapp.js
(function(){
'use strict';
angular.module('app', [])
.run(function($rootScope, $log) {
$rootScope.log = function(msg){
$log.info(msg);
}
})
.controller('LogCtrl', ['$scope', function LogCtrl($scope) {
$scope.logThis = function(msg){
$scope.$root.log(msg);
};
}]);
})();
test.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.5/angular.min.js"></script>
<script src="testapp.js"></script>
</head>
<body ng-app="app">
<div ng-controller="LogCtrl">
<p>Enter text and press the log button.</p>
Message:
<input type="text" ng-model="message"/>
<button ng-click="logThis(message)">log</button>
</div>
</body>
</html>
Injecting it seems impossible to me without defining it in the function parameters. But you can make it available:
var $log;
app.run(['$log',function(logService) {
$log = logService;
}]);
app.controller('MainCtrl', function($scope, myService) {
$log.warn('Controlling');
});
app.service('myService', function() {
$log.warn('Ha!');
return {};
});
http://plnkr.co/edit/Zwnay7dcMairPGT0btmC?p=preview
Another way would be to set it as a global variable (window.$log), but I wouldn't do that.
Here's a solution for those who think that using the $rootscope requires too much fuss: add the $log to the angular object.
angular.module('myModule')
.run(['$log', function($log) {
angular.log = $log;
}]);
Then when you create your controllers, no $log is required.
angular.module('myModule')
.controller('MyController', MyController);
MyController.$inject = []; // <-- see, no $log required!
function MyController() {
angular.log.info("Hello world");
}
You could even take it a step further and add angular.info = $log.info if you wish to shorten it a bit more.
I'm trying to make some basic tests on directives on an e2e scenario. The code works just fine
and I can render the new element to the browser. Here the code I'm using.
Here the directive code.
'use strict';
var directives = angular.module('lelylan.directives', [])
directives.directive('device', ['Device', function(Device) {
var definition = {
restrict: 'E',
replace: true,
templateUrl: 'js/templates/device.html',
scope: { id: '#' }
};
definition.link = function postLink(scope, element, attrs) {
scope.$watch('id', function(value){
var device = Device.get({ id: scope.id }, function() {
scope.device = device;
});
});
};
return definition
}]);
Here the Device service code.
// Service Device
'use strict';
angular.module('lelylan.services', ['ngResource']).
factory('Device', ['$resource', '$http', function($resource, $http) {
var token = 'df39d56eaa83cf94ef546cebdfb31241327e62f8712ddc4fad0297e8de746f62';
$http.defaults.headers.common["Authorization"] = 'Bearer ' + token;
var resource = $resource(
'http://localhost:port/devices/:id',
{ port: ':3001', id: '#id' },
{ update: { method: 'PUT' } }
);
return resource;
}]);
Here the app code.
'use strict';
angular.module('lelylan', ['lelylan.services', 'lelylan.directives'])
And here the index.html.
<!doctype html>
<html lang="en" ng-app="lelylan">
<head>
<meta charset="utf-8">
<title>Lelylan Components</title>
<link rel="stylesheet" href="css/app.css"/>
</head>
<body>
<device id="50c61ff1d033a9b610000001"></device>
<!-- In production use: <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js"></script> -->
<script src="lib/angular/angular.js"></script>
<script src="lib/angular/angular-resource.js"></script>
<script src="js/app.js"></script>
<script src="js/services.js"></script>
<script src="js/directives.js"></script>
</body>
</html>
After reading the Angular documentation and trying different solutions I came up with the following
test where I try mock my Backend requests. The problem is that the request still hits the real service. It
looks like I'm not able to intercept the requests.
// e2e test
'use strict';
describe('directives', function() {
var resource = { id: '1', uri: 'http://localhost:3001/devices/1' };
var myAppDev = angular.module('myAppDev', ['lelylan', 'ngMockE2E']);
myAppDev.run(function($httpBackend) {
$httpBackend.when('GET', 'http://localhost:3001/devices/1').respond(resource);
$httpBackend.when('GET', /\/templates\//).passThrough();
});
beforeEach(function() {
browser().navigateTo('../../app/index.html');
});
describe('when renders a device', function() {
it('renders the title', function() {
expect(element('.device .name').text()).toEqual('Closet dimmer');
});
it('renders the last time update', function() {
expect(element('.device .updated-at').text()).toEqual('2012-12-20T18:40:19Z');
})
});
});
I think I'm missing some configurations, but I can't really understand which ones.
Thanks a lot.
Reading the the last comment in this question I came with the final solution.
Actually I was missing one important step, due to the fact that I had to use an HTML file that uses the mocked application. Let's make the code speak.
1) I've created an HTML file with the test environment. The main difference is related to the fact that I've set the ng-app=test and I've added two new JS files. The first is /test/e2e/app-test.js and is where I've created the test module and the second is /test/lib/angular-mocks.js.
<!doctype html>
<html lang="en" ng-app="test">
<head>
<meta charset="utf-8">
<title>Lelylan Test</title>
<link rel="stylesheet" href="css/app.css"/>
</head>
<body>
<device id="1"></device>
<script src="lib/angular/angular.js"></script>
<script src="lib/angular/angular-resource.js"></script>
<script src="js/app.js"></script>
<script src="js/settings.js"></script>
<script src="js/services.js"></script>
<script src="js/controllers.js"></script>
<script src="js/filters.js"></script>
<script src="js/directives.js"></script>
<!-- Test application with mocked requests -->
<script src="../test/e2e/app-test.js"></script>
<script src="../test/lib/angular/angular-mocks.js"></script>
</body>
</html>
Now, lets see how we implement the test module. In here I define a module that is exactly as my main module (lelylan), with the addition of ngMockE2E that lets you access to $httpBackend and mock the HTTP requests.
'use strict';
var resource = { id: '1', uri: 'http://localhost:3001/devices/1' };
var test = angular.module('test', ['lelylan', 'ngMockE2E']);
test.run(function($httpBackend) {
$httpBackend.when('GET', 'http://localhost:3001/devices/1').respond(resource);
$httpBackend.when('GET', /\/templates\//).passThrough();
});
Nothing more. Run scripts/e2e-test.sh and you're done.