Injecting $attrs in AngularJS - angularjs

I have some JavaScript written in the context of AngularJS. My relevant JavaScript looks like the following:
.factory('$myFactory', function ($myLibrary, $interpolate) {
return {
myFunction: function ($scope, $attrs, p) {
if (p !== null) {
$attrs.set('myProperty', p);
}
}
};
I am trying to unit test this code. In an attempt to unit test the code, I'm using Jasmine.
it('should set myProperty', inject(function ($scope, $myFactory) {
// $myFactory.myFunction($scope
}));
I can't figure out how to inject some $attrs from my unit test. How do I do that? I can successfully get my $scope setup. However, I don't understand how to inject $attrs. Is there something special about it that I'm not aware of? I'm having a similar issue with $element, though that one is out of the context of this specific test.
Thank you!

Here is a plunker: http://plnkr.co/edit/k1cxSjpAXhhJUEOcx9PG
Maybe there is a better solution but That's what I got.
$scope is easy to get, you can inject $rootScope everywhere
$attrs on the other hand is only available through the $compile variable (it lives in compile.js)
My solution is to create a fake controller , to compile it and to hijack it's $attrs.
So that's how it looks like:
var injected = null
function fakeController($scope, $attrs, $element){
injected = {}
injected.$scope = $scope;
injected.$attrs = $attrs;
injected.$element = $element;
}
describe('Testing a Hello World controller', function() {
var $scope = null;
var $attrs = null;
var $element = null;
var ctrl = null;
//you need to indicate your module in a test
beforeEach(module('plunker'));
beforeEach(inject(function($compile, $rootScope, $controller) {
$compile('<span ng-controller="fakeController"></span>')($rootScope);
$scope = injected.$scope;
$attrs = injected.$attrs;
$element = injected.$element;
ctrl = $controller('MainCtrl', {
$scope: $scope,
$attrs: $attrs,
$element: $element
});
}));
it('should say hallo to the World', function() {
expect($scope.name).toEqual('World');
});
});

seems that you can just instantiate the controller with empty object, as well...
ctrl = $controller('MyCtrl', {
$scope: $scope,
$attrs: {}
});

In latest versions of AngularJS a controller needs to be provided to get full $attrs.
See $controllerProvider documentation.
Here's a snippet
describe('angular-component-controller', function() {
// save injected parameters of controller
var injected = {};
var controller;
// #see $https://docs.angularjs.org/api/ngMock/service/$componentController
// we need to extract those from $compile instead of use as locals
angular.module('locals', []).controller('controller',
['$attrs', function($attrs) {
injected.$attrs = $attrs;
}]
);
beforeEach(module('locals'));
beforeEach(inject(function($rootScope, $compile, $componentController) {
// invoke dummy component to get $attrs
$compile('<span ng-controller="controller">')($rootScope);
var locals = {};
locals.$scope = $rootScope.$new();
locals.$attrs = injected.$attrs;
var bindings = {};
controller = $componentController('component', locals, bindings);
}));
});
See this gist AngularJS $componentController unit test

Related

How to write test-case for Directive with in Directive using jasmine

I wrote a test for my directive using jasmine testcase framework with karma testcase runner.
In my project ,I am already having one directive called
<parent-directive></parent-directive>
and i tried to include that parent directive into another one called
<child-directive></child-directive>.
Parent directive elements are converted as components called SampleComponents and included in the child directive
Sample.js
'use strict'
angular.module('Sample')
.directive('SampleHeader', SampleHeader)
function SampleHeader () {
return {
restrict: 'A',
templateUrl: 'header/header.html',
scope: {},
controller: function ($scope) {
$scope.logoutHeader = function () {
console.log('Logout call back')
require('electron').remote.app.quit()
}
}
}
}
SampleSpec.js
describe('SampleHeader', function () {
var $compile, $rootScope, elements, scope, controller
beforeEach(module('Sample'))
beforeEach(module('SampleComponenets'))
beforeEach(module('ngAnimate'))
beforeEach(module('ngRoute'))
beforeEach(module('ngMaterial'))
beforeEach(module('ngCookies'))
beforeEach(module('datatables'))
beforeEach(inject(function (_$compile_, _$rootScope_, _$q_,_$controller_) {
deferred = _$q_.defer()
$compile = _$compile_
$rootScope = _$rootScope_
controller = _$controller_
scope = $rootScope.$new()
elements = angular.element('<sample-header></sample-header>')
$compile(elements)($rootScope.$new())
$rootScope.$digest()
controller = elements.controller('SampleHeader')
scope = elements.isolateScope() || elements.scope()
scope.$digest()
}))
it('should check logoutHeader is called', function () {scope.logoutHeader()
})
})
restrict: 'A' -> it seems you created a attribute directive so you should compile the directive as a attribute (like, elements = angular.element('<div sample-header></div>')).
describe('SampleHeader', function () {
var $compile, $rootScope, elements, scope, controller
beforeEach(module('Sample'))
beforeEach(module('SampleComponenets'))
beforeEach(module('ngAnimate'))
beforeEach(module('ngRoute'))
beforeEach(module('ngMaterial'))
beforeEach(module('ngCookies'))
beforeEach(module('datatables'))
beforeEach(inject(function (_$compile_, _$rootScope_, _$q_,_$controller_) {
deferred = _$q_.defer()
$compile = _$compile_
$rootScope = _$rootScope_
controller = _$controller_
scope = $rootScope.$new()
elements = angular.element('<div sample-header></div>')
$compile(elements)($rootScope.$new())
$rootScope.$digest()
controller = elements.controller('SampleHeader')
scope = elements.isolateScope() || elements.scope()
scope.$digest()
}))
it('should check logoutHeader is called', function () {scope.logoutHeader()
})
})

Unit Testing $routeParams in directive

I have a directive that accesses the $routeParams of the page as such:
myApp.directive("myList", function ($routeParams) {
return {
restrict: 'E',
templateUrl: 'tabs/my-list.html',
link: function (scope) {
scope.year = $routeParams.year;
}
};
});
The directive works as expected and correctly accesses the $routeParams
I am trying to test using angular-mock/jasmine. I can't figure out how to pass mock $routeParams to the directive. This is what I have:
describe('myList', function () {
var scope, compile, element, compiledDirective;
var mockParams = { 'year': 1996 };
beforeEach(function () {
module('templates', 'MyApp');
inject(function ($compile, $rootScope, $routeParams) {
compile = $compile;
scope = $rootScope.$new();
});
element = angular.element('<my-list></my-list>');
compiledDirective = compile(element)(scope);
scope.$digest();
});
it('should fill in the year', function () {
expect(scope.year).toEqual(mockParams.year);
});
});
Which obviously doesn't work because I never passed passed mockParams to the directive. Is there a way to do this?
Mock the $routeParams object mockParams using angular.extend OR do assign mockParams object directly to $routeParams. In that way $routeParams will be available before directive gets compiled.
inject(function ($compile, $rootScope, $routeParams) {
compile = $compile;
scope = $rootScope.$new();
angular.extend($routeParams, mockParams);
});

How to correctly accessing the scope and controller of a directive in a Jasmine unit test

The gist of my issue is that I don't think I am correctly accessing the scope and controller of my directive in accompanying unit tests.
I have a directive that looks something like this:
app.controller('GalleryDirectiveController', ['$scope', '$element', '$timeout', function($scope, $element, $timeout){
$scope.panels = [];
this.addPanel = function(panel){
// Timeout to help with $scope.$watch in other directives
return $timeout(function(){ $scope.panels.push(panel); }, 0);
};
}]);
app.directive('gallery', function(){
return {
restrict: 'E',
controller: 'GalleryDirectiveController'
};
});
What I am trying to do now is write unit tests that mock a collection of gallery panels that add themselves to the gallery. Unfortunately, I don't think I am correctly accessing the controller and scope of the directives in my unit tests, so they never pass.
The unit test looks something like this:
describe('app Gallery', function(){
var gallery;
var $scope;
beforeEach(module('app'));
beforeEach(inject(function($compile, $rootScope){
var element = angular.element('<gallery/>');
$compile(element)($rootScope.$new());
$rootScope.$digest();
$scope = element.isolateScope() || element.scope();
gallery = element.controller('gallery');
}));
it('should add panels to the gallery', function(){
for (var i = 0; i < 9; i++) {
gallery.addPanel({
$el : $('<div></div>')
});
}
// Each panel is added in a timeout, so I thought
// the problem might have been a timing issue
waitsFor(function(){
return $scope.panels.length !== 0;
}, 'the panels to be added', 200);
});
});
$scope.panels.length is always zero. This makes me think that the $scope variable I am setting in my unit test is not the same $scope being modified by the controller. For what it's worth, other unit tests that check if gallery.addPanel and $scope.panels are defined pass as expected.
Hopefully I am just missing something small. My fear is that I may have created a hard-to-test gallery directive.
You can flush the $timeout before setting your expectation.
i.e:-
var $timeout; //or var flushTimeout;
...
beforeEach(inject(function($compile, $rootScope, _$timeout_){ //<-- inject timeout
$timeout = _$timeout_; //Save it in outer scope
//or flushTimeout = _$timeout_.flush
...
and in your test do:-
it('should add panels to the gallery', function(){
for (var i = 0; i < 9; i++) {
gallery.addPanel({
$el : $('<div></div>')
});
}
$timeout.flush(); //or flushTimeout();
expect($scope.panels.length).toEqual(9);
});
Test Demo

Unit testing plugin directives with requirements

I'm using Videogular in an Angular app I'm working on. I wrote a plugin directive for it that listens to an event broadcast from $rootScope, and, if a video is playing, automatically pauses it when the event is broadcast.
omgYesDirectives.directive('vgAutoPause',
['$rootScope',
function($rootScope) {
return {
restrict: 'E',
require: '^videogular',
link: function($scope, $elem, $attr, $API) {
$rootScope.$on('onGameEnable', onGameEnable);
function onGameEnable(event, data)
{
$API.pause();
}
}
}
}
]);
But I'm having trouble figuring out how to unit test it. I can't seem to properly inject Videogular itself into my test. I've tried variations on this:
describe('vgAutoPause', function () {
var scope, compile, elm, videogular;
beforeEach(inject(function ($compile, $rootScope, videogularDirective) {
videogular = videogularDirective;
scope = $rootScope.$new();
compile = $compile;
}));
it('should instantiate as an HTML element', function () {
elm = compile('<videogular><vg-auto-pause></vg-auto-pause></videogular>')(scope);
scope.$digest();
expect(elm.html()).toContain('vg-auto-pause');
});
});
but Karma keeps complaining about it:
Error: [$injector:unpr] Unknown provider: videogularDirectiveProvider <- videogularDirective
Am I doing it wrong? Do you have any thoughts or suggestions on what I ought to be doing instead?
In AngularJS You can't inject a directive, you must create the HTML and then $compile it to start the $digest cycle.
For example, this is a simple videogular testing:
'use strict';
describe('Directive: Videogular', function () {
var element;
var scope;
beforeEach(module('myApp'));
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope;
element = angular.element("<div><videogular><video></video></videogular></div>");
$compile(element)($rootScope);
}));
describe("videogular", function() {
it("should have videogular", function() {
scope.$digest();
expect(element.html()).toContain('<video></video>');
});
});
});
Maybe you need to understand first how to test directives, there's a lot of good info out there. You can start with this links:
http://docs.angularjs.org/guide/unit-testing
https://egghead.io/lessons/angularjs-unit-testing-a-directive
http://angular-tips.com/blog/2013/08/watch-how-the-apply-runs-a-digest/

isolateScope() returns undefined when using templateUrl

I have a directive that I want to unittest, but I'm running into the issue that I can't access my isolated scope. Here's the directive:
<my-directive></my-directive>
And the code behind it:
angular.module('demoApp.directives').directive('myDirective', function($log) {
return {
restrict: 'E',
templateUrl: 'views/directives/my-directive.html',
scope: {},
link: function($scope, iElement, iAttrs) {
$scope.save = function() {
$log.log('Save data');
};
}
};
});
And here's my unittest:
describe('Directive: myDirective', function() {
var $compile, $scope, $log;
beforeEach(function() {
// Load template using a Karma preprocessor (http://tylerhenkel.com/how-to-test-directives-that-use-templateurl/)
module('views/directives/my-directive.html');
module('demoApp.directives');
inject(function(_$compile_, _$rootScope_, _$log_) {
$compile = _$compile_;
$scope = _$rootScope_.$new();
$log = _$log_;
spyOn($log, 'log');
});
});
it('should work', function() {
var el = $compile('<my-directive></my-directive>')($scope);
console.log('Isolated scope:', el.isolateScope());
el.isolateScope().save();
expect($log.log).toHaveBeenCalled();
});
});
But when I print out the isolated scope, it results in undefined. What really confuses me though, if instead of the templateUrl I simply use template in my directive, then everything works: isolateScope() has a completely scope object as its return value and everything is great. Yet, somehow, when using templateUrl it breaks. Is this a bug in ng-mocks or in the Karma preprocessor?
Thanks in advance.
I had the same problem. It seems that when calling $compile(element)($scope) in conjunction with using a templateUrl, the digest cycle isn't automatically started. So, you need to set it off manually:
it('should work', function() {
var el = $compile('<my-directive></my-directive>')($scope);
$scope.$digest(); // Ensure changes are propagated
console.log('Isolated scope:', el.isolateScope());
el.isolateScope().save();
expect($log.log).toHaveBeenCalled();
});
I'm not sure why the $compile function doesn't do this for you, but it must be some idiosyncracy with the way that templateUrl works, as you don't need to make the call to $scope.$digest() if you use an inline template.
With Angularjs 1.3, if you disable debugInfoEnabled in the app config:
$compileProvider.debugInfoEnabled(false);
isolateScope() returns undefined also!
I had to mock and flush the $httpBackend before isolateScope() became defined. Note that $scope.$digest() made no difference.
Directive:
app.directive('deliverableList', function () {
return {
templateUrl: 'app/directives/deliverable-list-directive.tpl.html',
controller: 'deliverableListDirectiveController',
restrict = 'E',
scope = {
deliverables: '=',
label: '#'
}
}
})
test:
it('should be defined', inject(function ($rootScope, $compile, $httpBackend) {
var scope = $rootScope.$new();
$httpBackend.expectGET('app/directives/deliverable-list-directive.tpl.html').respond();
var $element = $compile('<deliverable-list label="test" deliverables="[{id: 123}]"></deliverable-list>')(scope);
$httpBackend.flush();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
expect($element).toBeDefined();
expect($element.controller).toBeDefined();
scope = $element.isolateScope();
expect(scope).toBeDefined();
expect(scope.label).toEqual('test');
expect(scope.deliverables instanceof Array).toEqual(true);
expect(scope.deliverables.length).toEqual(1);
expect(scope.deliverables[0]).toEqual({id: 123});
}));
I'm using Angular 1.3.
You could configure karma-ng-html2js-preprocessor plugin. It will convert the HTML templates into a javascript string and put it into Angular's $templateCache service.
After set a moduleName in the configuration you can declare the module in your tests and then all your production templates will available without need to mock them with $httpBackend everywhere.
beforeEach(module('partials'));
You can find how to setup the plugin here: http://untangled.io/how-to-unit-test-a-directive-with-templateurl/
In my case, I kept running into this in cases where I was trying to isolate a scope on a directive with no isolate scope property.
function testDirective() {
return {
restrict:'EA',
template:'<span>{{ message }}</span>'
scope:{} // <-- Removing this made an obvious difference
};
}
function testWithoutIsolateScopeDirective() {
return {
restrict:'EA',
template:'<span>{{ message }}</span>'
};
}
describe('tests pass', function(){
var compiledElement, isolatedScope, $scope;
beforeEach(module('test'));
beforeEach(inject(function ($compile, $rootScope){
$scope = $rootScope.$new();
compiledElement = $compile(angular.element('<div test-directive></div>'))($scope);
isolatedScope = compiledElement.isolateScope();
}));
it('element should compile', function () {
expect(compiledElement).toBeDefined();
});
it('scope should isolate', function () {
expect(isolatedScope).toBeDefined();
});
});
describe('last test fails', function(){
var compiledElement, isolatedScope, $scope;
beforeEach(module('test'));
beforeEach(inject(function ($compile, $rootScope){
$scope = $rootScope.$new();
compiledElement = $compile(angular.element('<div test-without-isolate-scope-directive></div>'))($scope);
isolatedScope = compiledElement.isolateScope();
}));
it('element should compile', function () {
expect(compiledElement).toBeDefined();
});
it('scope should isolate', function () {
expect(isolatedScope).toBeDefined();
});
});

Resources