Given:
var myApp = angular.module('myApp', []);
myApp.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
classToAdd: '#'
},
template:
'<div class="{{classToAdd}}"></div>'
};
});
I'm testing a spec where classToAdd is being statically coded in the template:
<my-directive class-to-add="foo"></my-directive>
and the classToAdd attribute is only being recognized if I $digest $rootScope, and not $scope.
Why this is the case?
Working fiddle.
The reason the shown fiddle was failing, is that "foo" is bound to the $rootScope, not the local $scope.
The solution is to set the scope variable, interpolate it using {{foo}} (since we're using "#" in the isolate scope)
it('should bind to the class-to-add attribute when we $digest $scope', function () {
// Arrange
template = '<my-directive class-to-add="{{foo}}"></my-directive>';
compiledDirective = $compile(template)($scope);
directiveEl = compiledDirective.find('div');
$scope.foo = "bar"
// Act
$scope.$digest();
// Assert
expect(directiveEl.hasClass('bar')).toBe(true);
});
you can write your code like this:
angular.module('app',[])
.controller('ctrl',['$scope',function($scope){
$scope.myClass="myClass";
}])
.directive('myDirective',function(){
return {
restrict:'E',
scope:{ classToAdd: '#' },
transclude:true,
replace: true,
template:'<div class="{{classToAdd}}" ng-transclude></div>',
link: function(scope, iElement, iAttrs){
console.log(scope);
console.log(iAttrs);
}
}
})
and here is a working DEMO
notice the console for logs and under the hood stuff.
Related
function prMySelects() {
var ddo = {
restrict: 'E',
templateUrl: 'template.html',
require: '?ngModel',
scope: {
ngModel: '='
},
controller: prMySelectsController,
controllerAs: 'vm',
bindToController: true
};
return ddo;
}
function prMySelectsController($locale) {
...
}
I need do some checks inside directive controller and set ngModel.$setValidity('some', false), but getting ngModel is not defined error. Injecting ngModel didn't help...
PS I know that I can access it in link, but is it possible to reach ngModel controller in directive controller?
This sort of functionality is best done inside the link function for a directive.
function prMySelects() {
return {
restrict: 'E',
require: '?ngModel',
link: function(scope, element, attributes, ngModel) {
scope.theModel = ngModel;
},
controller: function() {
var vm = this;
vm.theModel.$setViewValue...
}
}
}
In this case, your are actually getting a hook into the ngModel controller and its not required that you actually specifiy it on the scope.
I am running my first test on a directive, and while it seems that the directive is being invoked (there is a GET request being made for the directive's template url), the DOM manipulation that is supposed to occur in the link function is not - well it's that or I'm just not testing it correctly.
// Generated by CoffeeScript 1.6.3
(function() {
var sprangularDirectives;
sprangularDirectives = angular.module('sprangularDirectives', []);
sprangularDirectives.directive('productDirective', function() {
return {
scope: {
product: '='
},
templateUrl: 'partials/product/_product.html',
link: function(scope, el, attrs) {
return el.attr('testattr', 'isthisclasshere');
}
};
});
}).call(this);
Test:
'use strict';
describe('productDirective', function() {
var scope, el, directive, $httpBackend, compiler, compiled, html;
beforeEach(angular.mock.module('sprangularApp'));
beforeEach(function() {
html = '<div data-product-directive product="currentProduct"></div>';
inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
// jasmine.getHTMLFixtures().fixturesPath='base/partials/product';
$httpBackend.when('GET', 'partials/product/_product.html').respond(
' <div class="product">'
+' {{ currentProduct.name }}'
+' {{ currentProduct.page }}'
+' </div>'
);
scope = $injector.get('$rootScope');
el = angular.element(html);
compiler = $injector.get('$compile');
compiled = compiler(el);
compiled(scope);
scope.$digest();
});
});
it('Should have an isolate scope', function() {
scope.currentProduct = {name: 'testing'};
console.log(el.attr('testattr'))
console.log(el.isolateScope())
expect(el.scope().product.name).toBe('testing');
});
});
console.log(el.attr('testattr')) returns undefined...even though when I boot my browser up it's there. Some help would be awesome :) thanks
The element you are using is the pre-compiled element reference. The element you want is returned from the "compiled(scope)" method call:
compiler = $injector.get('$compile');
compiled = compiler(el);
var element = compiled(scope); // <-- This guy!
I use this snippet as a testing helper method:
var compileTemplate = function (scope, rawTemplate) {
var template = angular.element(rawTemplate)
var element = $compile(template)(scope)
scope.$digest()
var new_scope = element.scope()
return [element, new_scope]
}
I am writing a REST app in Angular and I want to write unit tests for it (of course!). I have a controller which gets a list of blog posts from a REST service in json and puts the summaries into the $scope, so I can display them in the view.
At first the blog posts were just displaying as text ie <p>Blog body</p>, rather than rendering as parsed HTML, until I discovered that you can use ng-bind-html in conjunction with the $sce service. This now works fine in terms of displaying the blog posts correctly.
The problem arises when unit testing. I am trying to mock a json response with some HTML and then test that my controller is correctly dealing with the HTML. Here is my code:
Controller
.controller( 'HomeCtrl', function HomeController( $scope, $http, $sce ) {
$scope.posts = {};
$http.get('../drupal/node.json').success(function (data) {
var posts;
posts = data.list;
for(var i = 0; i < posts.length; i ++) {
posts[i].previewText = $sce.trustAsHtml(posts[i].body.summary);
posts[i].created = posts[i].created + '000'; // add milliseconds so it can be properly formatted
}
$scope.posts = posts;
});
})
unit test
describe('HomeCtrl', function() {
var $httpBackend, $rootScope, $sce, createController;
beforeEach(inject(function ($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
// Get hold of a scope (i.e. the root scope)
$rootScope = $injector.get('$rootScope');
// The $controller service is used to create instances of controllers
var $controller = $injector.get('$controller');
$sce = $injector.get('$sce');
createController = function() {
return $controller('HomeCtrl', {
'$scope': $rootScope
});
};
}));
it('should get a list of blog posts', function() {
var rawResponse = {
"list": [
{
"body": {
"value": "\u003Cp\u003EPost body.\u003C\/p\u003E\n",
"summary": "\u003Cp\u003ESummary.\u003C\/p\u003E\n"
},
"created": "1388415860"
}
]};
var processedResponse = [{
"body": {
"value": "\u003Cp\u003EPost body.\u003C\/p\u003E\n",
"summary": "\u003Cp\u003ESummary.\u003C\/p\u003E\n"
},
"created": "1388415860000",
previewText: $sce.trustAsHtml("\u003Cp\u003ESummary.\u003C\/p\u003E\n")
}];
$httpBackend.when('GET', '../drupal/node.json').respond(rawResponse);
$httpBackend.expectGET("../drupal/node.json").respond(rawResponse);
var homeCtrl = createController();
expect(homeCtrl).toBeTruthy();
$httpBackend.flush();
expect($rootScope.posts).toEqual(processedResponse);
});
});
When I run the above through the Karma test runner, I get the following response:
Chrome 31.0.1650 (Windows) home section HomeCtrl should get a list of blog posts FAILED
Expected [ { body : { value : '<p>Post body.</p>
', summary : '<p>Summary.</p>
' }, created : '1388415860000', previewText : { $$unwrapTrustedValue : Function } } ] to equal [ { body
: { value : '<p>Post body.</p>
', summary : '<p>Summary.</p>
' }, created : '1388415860000', previewText : { $$unwrapTrustedValue : Function } } ].
I suspect the problem is due to the fact that $sce.trustAsHtml returns an object containing a function, rather than a string.
My question is, firstly, am I approaching this problem in the correct way?
Secondly, if so, how should I go about testing the output of $sce.trustAsHtml?
Since the answer given by michael-bromley didn't work for me I want to point out another solution. In my case I was using a filter that wraps each occurrence of a string in another string with a span that has a class of 'highlight'. In other words, I want words to be highlighted. Here is the code:
angular.module('myModule').filter('highlight', function ($sce) {
return function (input, str) {
return $sce.trustAsHtml((input || '').replace(new RegExp(str, 'gi'), '<span class=\"highlighted\">$&</span>'));
};
});
I use the $sce service to trust the resulting value as HTML. To test this I need to use the $$unwrapTrustedValue function on the resulting value to get my test working:
it('01: should add a span with class \'highlight\' around each mathing string.', inject(function ($filter) {
// Execute
var result = $filter('highlight')('this str contains a str that will be a highlighted str.', 'str');
// Test
expect(result.$$unwrapTrustedValue()).toEqual('this <span class="highlighted">str</span> contains a <span class="highlighted">str</span> that will be a highlighted <span class="highlighted">str</span>.');
}));
UPDATE:
As #gugol kindly pointed out it is preferred not to use Angular internal methods like $$unwrapTrustedValue. A better approach is to use the public getTrustedHtml method on the $sce service. Like so:
it('01: should add a span with class \'highlight\' around each mathing string.', inject(function ($sce, $filter) {
// Execute
var result = $filter('highlight')('this str contains a str that will be a highlighted str.', 'str');
// Test
expect($sce.getTrustedHtml(result)).toEqual('this <span class="highlighted">str</span> contains a <span class="highlighted">str</span> that will be a highlighted <span class="highlighted">str</span>.');
}));
You have to disable $sce using its provider before each test.
When $sce is disabled all $sce.trust* methods just return original value instead of a wrapper function.
beforeEach(module(function ($sceProvider) {
$sceProvider.enabled(false);
}));
it('shall pass', inject(function($sce){
expect($sce.trustAsHtml('<span>text</span>')).toBe('<span>text</span>');
}));
In your particular example just do this:
describe('HomeCtrl', function() {
var $httpBackend, $rootScope, $sce, createController;
beforeEach(module(function ($sceProvider) {
$sceProvider.enabled(false);
}));
// rest of the file
});
I discovered that you can use $sce.getTrusted which will return the value originally passed to $sce.trustAsHtml, in this case a string containing HTML, which you can then test for equality in the usual way.
So my test now looks like this:
it('should create a previewText property using $sce.trustAsHtml', function() {
// confirms that it is an object, as should be the case when
// it has been through $sce.trustAsHtml
expect(typeof result.previewText === 'object').toEqual(true);
expect($sce.getTrusted($sce.HTML, result.previewText))
.toEqual('<p>Original HTML content string</p>');
});
Another option is to use the getTrustedHtml() function to get the html string value from $$unwrapTrustedValue.
vm.user.bio = $sce.getTrustedHtml(vm.user.bio);
What is the best approach to use Disqus in a single page application?
I see that the angular js docs has implemented it successfully.
Currently our approach looks like is this in our AngularJS app, but it seems unstable, is hard to test, and loads wrong thread ids (the same thread gets loaded almost everywhere).
'use strict';
angular.module('studentportalenApp.components')
.directive('disqusComponent',['$log', '$rootScope', function($log, $rootScope) {
var _initDisqus = function _initDisqus(attrs)
{
if(window.DISQUS) {
DISQUS.reset({
reload: true,
config: function () {
this.page.identifier = attrs.threadId;
this.disqus_container_id = 'disqus_thread';
this.page.url = attrs.permalinkUrl;
}
});
}
else
{
$log.error('window.DISQUS did not exist before directive was loaded.');
}
}
//Destroy DISQUS bindings just before route change, to properly dispose of listeners and frame (postMessage nullpointer exception)
$rootScope.$on('$routeChangeStart', function() {
if(window.DISQUS) {
DISQUS.reset();
}
});
var _linkFn = function link(scope, element, attrs) {
_initDisqus(attrs);
}
return {
replace: true,
template: '<div id="disqus_thread"></div>',
link: _linkFn
};
}]);
I also wanted to include Disqus on my AngularJS-powered blog. I found the existing solutions a bit unwieldy so I wrote my own directive:
.directive('dirDisqus', function($window) {
return {
restrict: 'E',
scope: {
disqus_shortname: '#disqusShortname',
disqus_identifier: '#disqusIdentifier',
disqus_title: '#disqusTitle',
disqus_url: '#disqusUrl',
disqus_category_id: '#disqusCategoryId',
disqus_disable_mobile: '#disqusDisableMobile',
readyToBind: "#"
},
template: '<div id="disqus_thread"></div>comments powered by <span class="logo-disqus">Disqus</span>',
link: function(scope) {
scope.$watch("readyToBind", function(isReady) {
// If the directive has been called without the 'ready-to-bind' attribute, we
// set the default to "true" so that Disqus will be loaded straight away.
if ( !angular.isDefined( isReady ) ) {
isReady = "true";
}
if (scope.$eval(isReady)) {
// put the config variables into separate global vars so that the Disqus script can see them
$window.disqus_shortname = scope.disqus_shortname;
$window.disqus_identifier = scope.disqus_identifier;
$window.disqus_title = scope.disqus_title;
$window.disqus_url = scope.disqus_url;
$window.disqus_category_id = scope.disqus_category_id;
$window.disqus_disable_mobile = scope.disqus_disable_mobile;
// get the remote Disqus script and insert it into the DOM
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + scope.disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
}
});
}
};
});
Advantages
The main advantage of this approach, I think, is that it keeps things simple. Once you have registered the directive with your app, you don't need to write any JavaScript or set any config values in your JavaScript. All configuration is handled by passing attributes in the directive tag like so:
<dir-disqus disqus-shortname="YOUR_DISQUS_SHORTNAME"
disqus-identifier="{{ article.id }}"
disqus-title="{{ article.title }}"
...>
</dir-disqus>
Also, you don't need to alter your index.html file to include the Disqus .js file - the directive will dynamically load it when it is ready. This means that all that extra .js will only get loaded on those pages that actually use the Disqus directive.
You can see the full source and documentation here on GitHub
Caveat
The above will only work properly when your site is in HTML5Mode, i.e. not using the "#" in your URLs. I am updating the code on GitHub so the directive will work when not using HTML5Mode, but be warned that you must set a hashPrefix of "!" to make "hashbang" URLs - e.g. www.mysite.com/#!/page/123. This is a limitation imposed by Disqus - see http://help.disqus.com/customer/portal/articles/472107-using-disqus-on-ajax-sites
I know nothing about Disqus, but according to the AngularJS Documentation source code:
They bind a load function to afterPartialLoaded:
$scope.afterPartialLoaded = function() {
var currentPageId = $location.path();
$scope.partialTitle = $scope.currentPage.shortName;
$window._gaq.push(['_trackPageview', currentPageId]);
loadDisqus(currentPageId);
};
Then, they simply add the html to the page:
function loadDisqus(currentPageId) {
// http://docs.disqus.com/help/2/
window.disqus_shortname = 'angularjs-next';
window.disqus_identifier = currentPageId;
window.disqus_url = 'http://docs.angularjs.org' + currentPageId;
// http://docs.disqus.com/developers/universal/
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = 'http://angularjs.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] ||
document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
angular.element(document.getElementById('disqus_thread')).html('');
}
This is how we solved it.
We load DISQUS in the body of index.html, and resets it whenever there is a directive using it.
Directive:
'use strict';
angular.module('fooApp.directives')
.directive('disqusComponent',['$window', '$log', function($window, $log) {
var _initDisqus = function _initDisqus(scope)
{
if($window.DISQUS) {
$window.DISQUS.reset({
reload: true,
config: function () {
this.page.identifier = scope.threadId;
this.disqus_container_id = 'disqus_thread';
}
});
}
else
{
$log.error('window.DISQUS did not exist before directive was loaded.');
}
}
var _linkFn = function link(scope, element, attrs) {
element.html('<div id="disqus_thread"></div>');
_initDisqus(scope);
}
return {
replace: true,
template: 'false',
scope: {
threadId: '#'
},
link: _linkFn
};
}]);
This is how it can be tested:
'use strict';
describe('Directive: Disqus', function() {
var element, $window, $rootScope, $compile;
beforeEach(function() {
module('fooApp.directives', function($provide) {
$provide.decorator('$window', function($delegate) {
$delegate.DISQUS = {
reset: jasmine.createSpy()
};
return $delegate;
});
});
inject(function(_$rootScope_, _$compile_, _$window_) {
$window = _$window_;
$rootScope = _$rootScope_;
$compile = _$compile_;
});
});
it('should place a div with id disqus_thread in DOM', function() {
element = angular.element('<disqus-component></disqus-component>');
element = $compile(element)($rootScope);
expect(element.html()).toBe('<div id="disqus_thread"></div>');
});
it('should do a call to DISQUS.reset on load', function() {
element = angular.element('<disqus-component thread-id="TESTTHREAD"></disqus-component>');
element = $compile(element)($rootScope);
var resetFn = $window.DISQUS.reset;
expect(resetFn).toHaveBeenCalled();
});
});
Here is the directive, that wraps jquery-ui autocomplete
angular.module('myApp.directives', [])
.directive('autocomplete', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
template: '<input ng-model="autocomplete" type="text"/>',
link: function (scope, element, attrs) {
scope.$watch(attrs.typedvalue, function () {
element.autocomplete({
search: function (event) {
scope[attrs.typedvalue] = this.value;
scope[attrs.fullselection] = '';
scope[attrs.selectionid] = '';
scope[attrs.shortselection] = '';
scope.$apply();
},
source: scope.fetchList,
select: function (event, ui) {
scope[attrs.fullselection] = ui.item.label;
scope[attrs.selectionid] = ui.item.itemId;
scope[attrs.shortselection] = ui.item.value;
scope.$apply();
}
});
});
}
};
});
I'm trying to unit-test it with the following test (following instructions from here https://github.com/vojtajina/ng-directive-testing):
describe('Directives', function () {
beforeEach(module('myApp.directives'));
describe('autocomplete directive', function () {
var elm, scope;
beforeEach(inject(function ($rootScope, $compile) {
elm = angular.element('<autocomplete fullselection="fullDstn" shortselection="dstn" selectionid="geonameId" typedvalue="typedValue" id="DstnSlctr"/>');
scope = $rootScope;
$compile(elm)(scope);
scope.$digest();
}));
it('should create input', inject(function ($compile, $rootScope) {
expect(elm.id).toBe('DstnSlctr');
expect(elm.prop('tagName')).toBe('INPUT');
debugger;
}));
});
});
But I get an error:
TypeError: Object [[object HTMLInputElement]] has no method 'autocomplete'
at Object.fn (C:/Users/kmukhort/Documents/_files/TMate/AngularTest/a
pp/js/directives.js:13:33)
on the line element.autocomplete({
I suspect that jquery-ui functionality is not attached to the element while $compile.
I'm referring jquery-ui library in testacular.config
basePath = '../';
files = [
...
'app/lib/jquery-ui-*.js',
];
Could you, please, tell, what I'm doing wrong?
Thanks!
Ksenia
I think you need to replace:
element.autocomplete(...);
With:
$(element).autocomplete(...);