Testing ui-grid wrapped in another directive - angularjs

I'm trying to create a directive that will allow me to set certain configurations to ui-grid as well as to add some functionality to it. I got something basic working, but the problem is that the jasmine test is giving me a hard time.
The JS code looks like this:
angular.module('myGridDirective', ['ui.grid'])
.directive('myGrid', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'templates/my-grid.html'
};
});
The template looks like this:
<div><div id="test-id" class="gridStyle" ui-grid="gridOptions"></div></div>
And the test looks like this:
describe('my grid directive', function() {
var $compile,
$rootScope;
beforeEach(module('myGridDirective'));
beforeEach(module('templates/my-grid.html'));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
});
});
it('Replaces the element with an ui-grid directive element', function() {
var element = $compile("<my-grid></my-grid>")($rootScope);
$rootScope.$digest();
expect(element.html()).toEqual('<div id="test-id" class="gridStyle" ui-grid="gridOptions"></div>');
});
});
The problem is that, while the directive is working (i.e. using <my-grid></my-grid> anywhere in my html file works), the test is failing.
I get the error message:
TypeError: $scope.uiGrid is undefined in .../angular-ui-grid/ui-grid.js (line 2879)
The relevant line in ui-grid.js is (the first line is 2879):
if (angular.isString($scope.uiGrid.data)) {
dataWatchCollectionDereg = $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction);
}
else {
dataWatchCollectionDereg = $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction);
}
The thing is, if I replace the ['ui.grid'] array in the directive module creation with an empty array, the test passes. The only problem, is that if I do that, I'll have to include 'ui.grid' anywhere the directive is used otherwise the directive stops working, which is something I cannot do.
I already tried transcluding, but that didn't seem to help, not to mention that the directive itself works, so it doesn't seem logical to have to do that just for the test.
Any thoughts ?

Ok, I figured out the solution.
At first found one way to solve this, which is:
Initialize the gridOptions variable with some data so that the ui-grid will get constructed
However, once I got that to work, I tried to add 'expect' statements, when it hit me that now I have a lot of 3rd party html to test, which is not what I want.
The final solution was to decide to mock the inner directive (which should be tested elsewhere), and to use the mock's html instead.
Since ngMock does not support directives, I found this great article explaining how to mock directives using $compileProvider, which solved my problem altogether.

Related

how to insert an angular 1.5 component with ng-bind-html

I have a component, and i would like to inject it dynamically into my html.
I have a component like this:
angular.module('test1', []);
angular.module('test1').component('test1', {
templateUrl: 'components/test1/test1.template.html',
controller: function test1Controller($scope) {
}
});
the test1.template.html file looks like this:
<p>TEST 1</p>
on my controller i have this:
angular.module('myApp')
.controller('ctrlCtrl', function ($scope, $sce) {
$scope.tag = "<test1/>";
});
on my index.html, i have this:
<ng-bind-html ng-bind-html="tag"></ng-bind-html>
but the tag will not show up. I have tried writing literaly "'<p>hi!</p>'" on the ng-bind-html field, and the text "hi!" shows up on a paragraph, so i don't think this error is because of a typo.
I also tried to use $sce.trustAsHtml, but it didn't work neither :(
$scope.tag = $sce.trustAsHtml("<test1/>");
when i insert an input field, the trustAsHtml method does work, but when i try to inject my components dynamically, it just won't let me, please help D:
Why ng-include won't work?
Components need to be compiled before you can use them on the markup. Try editing the html of the app with the developer tools from your browser, by artificially injecting your component on the markup: it won't work.
How to dynamically include components?
you'll need to use directives, this tutorial (thanks to #Artem K.) is friendly to follow, but you can also read the angular's official documentation, it is a little hard to understand though.
Following the logic of the final example of the angular's official documentation, you can create a directive that compiles everything that is passed to it, like this:
// source: https://docs.angularjs.org/api/ng/service/$compile
angular.module('myApp')
.directive('my-compile', function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
});
and then, on your index.html, you'll have to invoke the directive, sending the the string containing the component's tag as an argument.
<div compile="tag"></div>
As #charlietfl and #Artem K. said, you have to understand the angular's $compile so, thanks guys for pointing me in the right direction :)

Using scope in directive and service

I am trying to create a developer console in my Angular app and I have the following problem.
On every page of my application I want my developer console to be available (like a floating thing in a corner of the screen). I have HTML template and I am switching content with ui-view. My controllers can include consoleService and then call something like myConsole.log(...). This function should add message to some array and then display it on web. For my console I have directive with HTML template, where I want to use ng-repeat to display all messages.
And now my problem. How should I design this? I was thinking like this:
First solution
Well, I can create global JS variable, store all messages there and then use <script> and write em out in my directive template.
WRONG! Not respecting Angular-way of doing things
Second solution
Ok, I`ll use $scope and put my messages there, and then I can use that magical Angular data bind to just let it work on its own. Ok, but here I have a problem, how can I inject $scope into my service?
WRONG! Injecting $scope into service is nonsense, because it would not make any sense, and again, it kinda oppose the Angular-way.
Third solution
Fine, lets store my messages on console service, since all services in Angular are singletons it should work pretty fine, but... there is no $scope inside a service, so I need to return this array of messages to a controller and then assign it to $scope in controller.
??? What do you think? My problem with this solution is that in every controller I need to assign message array to $scope and I just dont want to do that, since I need this console everywhere, on every page.
I hope that my explanation of what I want to do is clear, so I am just hoping for a hint, or advice on how to design it in Angular.
Use the third solution. Your service will have an array with all the messages and each controller can use one of it's functions (service.log(...)) which will basically just add one message to the service list.
Then on your directive, you just assign the getMessages to the directive scope:
var app = angular.module('plunker', []);
app.service('log', function(){
var messages = [];
return {
getMessages: function() { return messages; },
logMessage: function(msg) { messages.push(msg); }
}
})
app.controller('MainCtrl', function($scope, log) {
$scope.msg = '';
$scope.addMessage = function() {
log.logMessage($scope.msg);
};
});
app.directive('console', function(log) {
return {
restrict: "E",
scope: {},
replace: true,
template: "<div><div ng-repeat='msg in messages'>{{msg}}</div></div>",
link: function(scope, el, attr) {
scope.messages = log.getMessages();
}
};
});
plunker: http://plnkr.co/edit/Q2g3jBfrlgGQROsTz4Nn?p=preview

Jasmine unit test for Angular directive

I have the following angular directive, which adds a tooltip when I hover over a span.
angular.module('mainMod')
.directive('toolTip', [function() {
return {
restrict: 'A',
scope: {
theTooltip: '#toolTip'
},
link: function(scope, element, attrs){
element.tooltip({
delay: 0,
showURL: false,
bodyHandler: function() {
return jQuery('<div class="hover">').text(scope.theTooltip);
}
});
}
}
}])
;
<span ng-show="data.tooltip" class="icon" tool-tip="{{data.tooltip}}"></span>
I'm looking to write a unit test for this directive, atm I can't use jasmine-jquery.
I'm fairly new to writing unit tests, could anyone possibly help me out?
Give me some pointers or point me towards some helpful resources?
Any advice or suggestions would be greatly appreciated.
What I have atm isn't much...
describe('Unit testing tooltip', function() {
var $compile;
var $rootScope;
// Load the myApp module, which contains the directive
beforeEach(module('mainMod'));
// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject(function(_$compile_, _$rootScope_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it(' ', function() {
// Compile a piece of HTML containing the directive
FAILS HERE --> var element = $compile("<span class='icon' tool-tip='{{data.tooltip}}'></span>")($rootScope);
$rootScope.$digest();
});
});
It's failing with a message of
TypeError: undefined is not a function
I think it's being caused by the ($rootScope) at the end of the line I've specified above.
You have to wrap your DOM content with angular.element first before compiling it. I am not sure what the tooltip module you are using but I used the jQuery UI tooltip instead.
//create a new scope with $rootScope if you want
$scope = $rootScope.$new();
var element = angular.element("<span class='icon' tool-tip='This is the tooltip data'></span>");
//use the current scope has just been created above for the directive
$compile(element)($scope);
One more thing, because you are using isolate scope in your directive, to get the current scope from your directive, you need to call
element.isolateScope()
based on this reference : How to Unit Test Isolated Scope Directive in AngularJS
For a working fiddle, you can found it here : http://jsfiddle.net/themyth92/4w52wsms/1/
Any unit test is basically the same - mock the environment, construct the unit, check that the unit interacts with the environment in the expected manner. In this instance, you'd probably want to mock the tooltip creator
spyOn(jQuery.fn, 'tooltip');
then compile some template using the directive (which you're already doing), then simulate the hover event on the compiled element and then check that the tooltip creator was called in the expected manner
expect(jQuery.fn.tooltip).toHaveBeenCalledWith(jasmine.objectContaining({
// ... (expected properties)
}));
How you simulate the event depends on how the element.tooltip is supposed to work. If it really works the way you're using it in the question code, you don't need to simulate anything at all and just check the expected interaction right after template compilation.

Angular: ng-bind-html filters out ng-click?

I have some html data that I'm loading in from a json file.
I am displaying this html data by using ngSanitize in my app and using ng-bind-html.
Now I would like to convert any links in the json blob from the standard
link
to:
<a ng-click="GotoLink('some_link','_system')">link</a>.
So I'm doing some regExp on the json file to convert the links, but for some reason however ng-bind-html is filtering out the ng-click in it's output, and I can't figure out why. Is it supposed to do this, and if so is it possible to disable this behavior?
Check out this jsFiddle for a demonstration:
http://jsfiddle.net/7k8xJ/1/
Any ideas?
Ok, so the issue is that it isn't compiling the html you include (angular isn't parsing it to find directives and whatnot). Can't think of a way to make it to compile from within the controller, but you could create a directive that includes the content, and compiles it.
So you would change
<p ng-bind-html="name"></p>
to
<p compile="name"></p>
And then for the js:
var myApp = angular.module('myApp', ['ngSanitize']);
angular.module('myApp')
.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.compile);
},
function(value) {
element.html(value);
$compile(element.contents())(scope);
}
)};
}]).controller('MyCtrl', function($scope) {
var str = 'hello http://www.cnn.com';
var urlRegEx = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+#)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+#)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-]*)?\??(?:[\-\+=&;%#\.\w]*)#?(?:[\.\!\/\\\w]*))?)/g;
result = str.replace(urlRegEx, "<a ng-click=\"GotoLink('$1',\'_system\')\">$1</a>");
$scope.GotoLink = function() { alert(); }
$scope.name = result;
});
Angular 1.2.12: http://jsfiddle.net/7k8xJ/4/
Angular 1.4.3: http://jsfiddle.net/5g6z58yy/ (same code as before, but some people were saying it doesn't work on 1.4.*)
I still faced some issue with the compile, as that was not fulfilling my requirement. So, there is this, a really nice and easy hack to work around this problem.
We replace the ng-click with onClick as onClick works. Then we write a javascript function and call that on onClick event.
In the onClick function, we find the scope of the anchor tag and call that required function explicitly.
Below is how its done :)
Earlier,
<a id="myAnchor" ng-click="myControllerFunction()" href="something">
Now,
<a id="myAnchor" onClick="tempFunction()" href="something">
at the bottom or somewhere,
<script>
function tempFunction() {
var scope = angular.element(document.getElementById('myAnchor')).scope();
scope.$apply(function() {
scope.myControllerFunction();
});
}
</script>
This should work now. Hope that helps someone :)
For more info, see here.
Explicitly Trusting HTML With $sce
When you want Angular to render model data as HTML with no questions asked, the $sce service is what you’ll need. $sce is the Strict Contextual Escaping service – a fancy name for a service that can wrap an HTML string with an object that tells the rest of Angular the HTML is trusted to render anywhere.
In the following version of the controller, the code asks for the $sce service and uses the service to transform the array of links into an array of trusted HTML objects using $sce.trustAsHtml.
app.controller('XYZController', function ($scope, $sce) {
$sce.trustAsHtml("<table><tr><td><a onclick='DeleteTaskType();' href='#workplan'>Delete</a></td></tr></table>");

Angular directive not linking in test

To get familiar with directive testing I created the simple example shown below. Unfortunately, the test is failing and it seems that the link function is never called. The directive does work when used within an app.
I have tried hardcoding the message attribute, removing the condition within the link function and even extracting the attr set from within the $watch, but the test still fails.
There has been other posts like this and the reason for those was due to the lack of a $digest call, but I do have that and I have tried moving it into the it spec block.
If I run a console.log(elem[0].otherHTML) call the scope binding seems to work
<wd-alert type="notice" message="O'Doole Rulz" class="ng-scope"></wd-alert>
What am I missing?
alert.spec.js
"use strict";
describe('Alert Specs', function () {
var scope, elem;
beforeEach(module('myapp'));
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope;
scope.msg = "O'Doole Rulz";
elem = angular.element('<wd-alert type="notice" message="{{msg}}"></wd-alert>');
$compile(elem)(scope);
scope.$digest();
}));
it('shows the message', function () {
expect(elem.text()).toContain("O'Doole Rulz");
});
});
alert.js
angular.module('myapp').directive('wdAlert', function() {
return {
restrict: 'EA',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
attrs.$observe('message', function() {
if (attrs.message) {
element.text(attrs.message);
element.addClass(attrs.type);
}
})
}
}
});
Turns out the issue was a Karma configuration for the way I had the files organized. I will leave this question up just in case it bites someone else in the ass.
files: [
...,
'spec_path/directives/*js' // what I originally had
'spec_path/directives/**/*.js' // what I needed
]
I'm adding this to expand on the answer for anyone else who wants to know.
I installed a third-party directive via bower. It worked in the app, but caused tests to break.
The problem is that Grunt/Karma doesn't read the scripts in your index.html. Instead, you need to make sure you let karma know each installed script.
For example, I used the directive ngQuickDate (a datepicker). I added it to index.html for the app, but I needed to add it to karma.conf.js as well:
files: [
...
'app/bower_components/ngQuickDate/dist/ng-quick-date.js'
]
The answer above does the same thing by means of using the ** wildcard to mean "in all subdirectories recursively".
Hope that helps!

Resources