Jasmine - How to test a stylesheet directive - angularjs

I have written a directive that interrogates the current URL, and loads the corresponding freestyle. The problem i am facing is how can i test the link function:
HTML:
<html lang="en" ng-app="myApp">
<head>
<meta charset="UTF-8">
<title>CSS Test</title>
<link get-style-directive />
</head>
Directive:
app.directive('getStyleDirective', ["$compile", "$location",
function($compile, $location) {
return {
replace: true,
scope: true,
link: function(scope, element, attrs) {
var urlValue = $location.absUrl();
var getCSSFile;
if (urlValue.indexOf("red") > -1) {
getCSSFile= 'red';
}
else if (urlValue.indexOf("blue") > -1) {
getCSSFile= 'blue';
}
else if (urlValue.indexOf("orange") > -1) {
getCSSFile= 'orange';
}
else {
getCSSFile = 'black';
}
var jqLiteWrappedElement = angular.element('<link href="../asset/css/' + getCSSFile + '.css" rel="stylesheet" />');
element.replaceWith(jqLiteWrappedElement);
$compile(jqLiteWrappedElement)(scope);
}
};
}]);
Test:
describe('Unit testing great quotes', function() {
var $compile,
$rootScope;
beforeEach(module('myApp'));
beforeEach(inject(function(_$compile_, _$rootScope_){
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('Replaces the element with the appropriate content', function() {
var element = $compile("<link get-style-directive />")($rootScope);
console.log(element);
$rootScope.$digest();
it('should have 1 link tag', function () {
expect(element[0].querySelectorAll('link').length).toEqual(1);
});
//it('if urlValue is blank getCSSFile should be black', function () {
//
//});
});
});
The above test was failing so i added a console.log to element and can see in the console:
Object[link.ng-scope]

Related

Angular directive compile: "RangeError: Maximum call stack size exceeded"

I want to add an 'ng-pattern' directive to an input element through a custom directive. I don't want to do it in the templates directly, but it looks I'm getting in a infinite loop.
I tried to set first the 'html' and compile the element after (Angular compile in directive seems to go into infinite loop) but scope is undefined. I don't know if it's related with replacing element's content.
Should i create a new scope? Do I'm missing something?
Thanks in advance!
var myHtml = iElem[0].outerHTML;
iElem.replaceWith(myHtml);
var compiledElement = $compile(iElem)(iElem.scope());
HTML:
<input type="text" ng-model="personal.testNumber_string" my-model="personal.testNumber" dot-to-comma>
Directive:
function dotToCommaConverter($compile) {
return {
require: 'ngModel',
restrict: 'A',
scope: {
myModel: '='
},
controllerAs: 'dot2Comma',
controller: function($scope) {
this.myModel = $scope.myModel;
},
compile: function(tElem, tAttrs) {
return {
pre: function(scope, iElem, iAttrs) {
},
post: function(scope, iElem, iAttrs, modelCtrl) {
iElem.attr('ng-pattern', '/^-?[0-9]+(?:\,[0-9]+)?$/');
var compiledElement = $compile(iElem)(iElem.scope());
iElem.replaceWith(compiledElement);
modelCtrl.$setViewValue(String(scope.dot2Comma.myModel).replace('.', ','));
modelCtrl.$render();
modelCtrl.$parsers.push(function(inputValue) {
var transformedInput = inputValue.replace(/[^0-9,.-]/g, '');
transformedInput = transformedInput.replace('.', ',');
transformedInput = transformedInput.replace(' ', '');
if (transformedInput !== inputValue) {
modelCtrl.$setViewValue(transformedInput);
modelCtrl.$render();
}
if (!isNaN(Number(transformedInput.replace(',', '.')))) {
scope.myModel = Number(transformedInput.replace(',', '.'));
} else {
scope.myModel = undefined;
}
return transformedInput;
});
}
};
}
};
}
I needed to remove my own directive from the Html content before re-compiling again, that's what caused the infinite loop.
iElem.removeAttr('dot-to-comma');
iElem.attr('ng-pattern', '/^-?[0-9]+(?:\,[0-9]+)?$/');
iElem.attr('ng-blur', 'dot2Comma.myBlurFunction()');
var compiledElement = $compile(iElem)(scope);
iElem.replaceWith(compiledElement);
here is an sample directive which replace dots with commas in a textbox :
script.js
angular.module('app', []);
angular.module('app')
.controller('ExampleController', ['$scope', function($scope) {
$scope.my = { number: '123.456' };
}]);
angular.module('app')
.directive('dotToComma', function() {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(attrs.ngModel, function (value) {
var newValue = value.replace('.', ',');
element.val(newValue);
});
}
}
});
index.html
<html lang="en" ng-app="app">
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<form ng-controller="ExampleController">
<p>scope.my.number = {{my.number}}</p>
<label>In this textbox, dots will automatically be replaced with commas, even if you change its value :</label>
<input type="text" ng-model="my.number" dot-to-comma>
</form>
</body>
</html>
Here is a plunker : https://plnkr.co/edit/X6Fi0tnjBXKKhbwH0o2q?p=preview
Hope it helps !

Get instance of directive controller with Angular/Jasmine

When unit testing angular directives, how can I grab an instance of the directive controller and assert certain data bindings on the controller?
function myDirective() {
return {
restrict: 'E',
replace: true,
templateUrl: 'tpl.html',
scope: { },
controller: function($scope) {
$scope.text = 'Some text';
}
};
}
angular
.module('example')
.directive('myDirective', [
myDirective
]);
unit test
describe('<my-directive>', function() {
var element;
var $compile;
var $scope;
beforeEach(module('example'));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$scope = _$rootScope_.$new();
});
});
describe('basic functionality', function() {
beforeEach(function() {
element = $compile('<my-directive></my-directive')($scope);
$scope.$digest();
});
it('should bind the correct text', function() {
//?
});
});
});
element = $compile('<my-directive></my-directive')($scope);
$scope.$digest();
ctrl = element.controller('myDirective');
Call element.controller with $scope like element.controller($scope). Some proof of concept bellow.
angular
.module('example', [])
.directive('myDirective', [
function myDirective() {
return {
restrict: 'E',
replace: true,
//template: ['<div>{{ text }}</div>'].join(''),
scope: {},
controller: function($scope) {
$scope.text = 'Some text';
}
};
}
]);
describe('<my-directive>', function() {
var element;
var $compile;
var $scope;
beforeEach(module('example'));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$scope = _$rootScope_.$new();
});
});
describe('basic functionality', function() {
beforeEach(function() {
element = $compile('<my-directive></my-directive')($scope);
$scope.$digest();
});
it('should bind the correct text', function() {
expect(element.controller($scope).scope().$$childTail.text).toEqual('Some text')
});
});
});
<link href="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine.css" rel="stylesheet" />
<script src="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine-2.0.3-concated.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-mocks.js"></script>

Using ng-hide on a loader in a directive in Angular

I've been trying to replicate the loaing example on this page (Showing Spinner GIF during $http request in angular) by #punkrockpolly. But either the example is wrong or I'm not getting something.
Basically i have a directive
.directive('loading', ['$http', function ($http) {
var html = '<div class="loader" data-loading></div>';
return {
restrict: 'E',
replace: true,
template: html,
link: function (scope, element, attrs) {
console.log(element);
scope.isLoading = function () {
console.log("is loading");
return $http.pendingRequests.length > 0;
};
scope.$watch(scope.isLoading, function (value) {
console.log(value);
if (value) {
element.removeClass('ng-hide');
} else {
element.addClass('ng-hide');
}
});
}
};
}]);
That I'm trying to turn on or off based on the $http request.
Here's what I have on my HTML page
<loading></loading>
What am I missing here.
Yes it works. I don't have the css for the spinner, but the ng-hidepart works perfect.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
app.directive('loading', ['$http', function ($http) {
var html = '<div class="loader" data-loading>test</div>';
return {
restrict: 'E',
replace: true,
template: html,
link: function (scope, element, attrs) {
scope.isLoading = function () {
// console.log("is loading");
return $http.pendingRequests.length > 0;
};
// just to have some pending requests
setInterval(function(){
$http.get("https://www.google.com").then(function(){
});
},200)
scope.$watch(scope.isLoading, function (value) {
// console.log(value);
if (value) {
element.removeClass('ng-hide');
} else {
element.addClass('ng-hide');
}
});
}
};
}]);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link href="style.css" rel="stylesheet" />
<script data-semver="1.4.9" src="https://code.angularjs.org/1.4.9/angular.js" data-require="angular.js#1.4.x"></script>
<script src="app.js"></script>
</head>
<body ng-app="plunker" ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<loading></loading>
</body>
</html>
To get app wide spinner use angular interceptors
angular.module("MyApp", ["ngResource"])
.config(function ($httpProvider) {
$httpProvider.interceptors.push(function($q, $rootScope) {
return {
'request': function(config) {
$rootScope.$broadcast('http:request:start');
return config;
},
'response': function(response) {
$rootScope.$broadcast('http:request:end');
return $q.reject(response);
}
};
});
})
.directive('loading', ['$http', function ($http) {
var html = '<div class="loader" data-loading></div>';
return {
restrict: 'E',
replace: true,
template: html,
link: function (scope) {
scope.isLoading = false;
scope.$on('http:request:start', function() {
scope.isLoading = true;
});
scope.$on('http:request:end', function() {
scope.isLoading = false;
});
}
};
}]);

Sharing logic between directives

I'm trying to share the logic between these two directives, I'm still learning Angular and don't quite understand how to accomplish this. I'm getting a $compile:ctreq error. I have watched some tutorials and I believe the logic is supposed to be in the controller but I get an error and the page wont load. I have a simple Pomodoro timer and would like the buttons to each be there own directive. Maybe I should be doing this with controllers but either way I would like to know how this works. Thanks..
var app = angular.module('pomodoro_timer', ['ui.router', 'firebase']);
app.directive("timer", ['$interval', function($interval) {
return {
restrict: "E",
transclude: true,
controller: function() {
},
templateUrl: "/templates/timer.html",
link: function(scope,element,attributes) {
scope.intrvl;
scope.t = 10;
var tDiv = $(element).find('#time');
scope.min = "25";
scope.sec = "00";
scope.interval = function() {
scope.intrvl = $interval(function(){
if (scope.t == 0) {
scope.resetTimer();
scope.sessionComplete = false;
} else {
scope.t -= 1;
scope.displayTime()
}
},1000)
}
scope.toggleClass = function() {
tDiv.toggleClass('notWorking working');
}
}
};
}]);
app.directive('start', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
require: "^timer",
templateUrl: '/templates/start.html',
link: function (scope, element, attr, timerCtrl) {
scope.startTimer = function() {
if (tDiv.hasClass("notWorking")) {
// scope.working = true;
scope.interval(scope.t);
scope.toggleClass();
}
};
}
};
});
HTML
<!DOCTYPE html>
<html ng-app="pomodoro_timer">
<head lang="en">
<title>Pomodoro Timer</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/style.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.8/angular-ui-router.min.js"></script>
<script src="https://cdn.firebase.com/js/client/2.2.4/firebase.js"></script>
<script src="https://cdn.firebase.com/libs/angularfire/1.1.1/angularfire.min.js"></script>
</head>
<body>
<timer></timer>
<start></start>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="application/javascript" src="/js/app.js"></script>
</body>
</html>
As you are using require: '^timer' inside start, that means you are assuming that your start directive should be inside timer directive so that start directive can get access to the controller of timer directive.
Also you should place the expo-sable method inside controller rather than placing it into link function, controller could be accessible by the directive which require this controller.
Markup
<timer>
<start></start>
</timer>
Code
app.directive("timer", ['$interval', function($interval) {
return {
restrict: "E",
transclude: true,
controller: function($scope) {
$scope.interval = function() {
$scope.intrvl = $interval(function() {
if (scope.t == 0) {
$scope.resetTimer();
$scope.sessionComplete = false;
} else {
$scope.t -= 1;
$scope.displayTime()
}
}, 1000)
};
$scope.toggleClass = function() {
tDiv.toggleClass('notWorking working');
};
},
templateUrl: "/templates/timer.html",
link: function(scope, element, attributes) {
scope.intrvl = 0; //set default value
scope.t = 10;
var tDiv = $(element).find('#time');
scope.min = "25";
scope.sec = "00";
}
};
}]);
app.directive('start', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
require: "^timer",
templateUrl: '/templates/start.html',
link: function(scope, element, attr, timerCtrl) {
scope.startTimer = function() {
if (tDiv.hasClass("notWorking")) {
//calling method of `timer` directive controller
timerCtrl.interval(scope.t);
timerCtrl.toggleClass();
}
};
}
};
});

test directive with ui-router $state.current

Trying to test a directive but I'm at a loss. Basically I don't know how to set the test up and I can't find any examples to step by step me. Can someone provide an explanation of how this should be set up? Right now the error I'm getting is
TypeError: 'undefined' is not an object (evaluating 'current.name')
directives.directive('convenienceNav', function(){
return {
restrict: 'A',
template: '<button class="btn btn-success" ui-sref=" {{$state.current.name}}.add"><i class="fa fa-user-plus"></i>Add {{$state.current.params.singular_title}}</button>'
};
});
describe('directive: convenienceNav', function() {
var element, scope, stateParams, state;
beforeEach(module('app.directives'));
beforeEach(module('ui.router'));
beforeEach(inject(function($rootScope, $compile, $state) {
scope = $rootScope.$new();
stateParams = {'api_resource_name': 'people'};
state = $state;
state.current.name = 'admin.people';
// state.current.params.singular_title = 'Person';
element ='<div convenience-nav></div>';
element = $compile(element)(scope);
scope.$digest();
}));
it('should have state.current.name = admin.people', function(){
expect(element.html()).toBe('<button class="btn btn-success" ui-sref="admin.people.add"><i class="fa fa-user-plus"></i>Add Person</button>');
});
});
Below are the examples of how you can test angular directives and routes navigation(ui router) in jasmine.
Button with name
var namedButtonModule = angular.module('namedButtonModule', []);
namedButtonModule.directive('namedButton', function() {
return {
restrict: 'AE',
scope: {
name: "=?"
},
template: '<button class="btn btn-success">I am {{name}} Button</button>',
controller: function($scope) {
$scope.name = $scope.name || 'simple';
}
};
});
describe('directive: namedButton', function() {
beforeEach(module('namedButtonModule'));
beforeEach(inject(function($rootScope, $compile) {
this.$rootScope = $rootScope;
this.$compile = $compile;
this.testContainer = document.getElementById('test-container');
this.compileDirective = function(template, scope) {
var element = this.$compile(template)(scope);
this.testContainer.appendChild(element[0]);
scope.$digest();
return element;
}
}));
afterEach(function() {
this.testContainer.innerHTML = '';
});
it('button should show default name', function() {
var template = '<div named-button></div>';
var scope = this.$rootScope.$new();
var element = this.compileDirective(template, scope);
expect(element.text()).toBe('I am simple Button');
});
it('button should show passed name to the scope', function() {
var template = '<div named-button name="inputName"></div>';
var scope = this.$rootScope.$new();
scope.inputName = "Angular Test";
var element = this.compileDirective(template, scope);
expect(element.text()).toBe('I am Angular Test Button');
});
});
<!-- jasmine -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine-html.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/boot.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.min.css" rel="stylesheet" />
<!-- angular -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular-mocks.js"></script>
<div id="test-container"></div>
Navigation Button
on clicking button, app will redirect to that state.
var navigationButtonsModule = angular.module('navigationButtonsModule', ['ui.router']);
navigationButtonsModule.config(['$stateProvider',
function($stateProvider) {
$stateProvider.state('home', {
url: '/home',
templateUrl: 'home.html'
});
}
]);
navigationButtonsModule.directive('navigationButton', function() {
return {
restrict: 'AE',
scope: {
state: "=?",
name: "=?"
},
template: '<button class="btn btn-success" ui-sref="{{state}}">Go to {{name}}</button>',
};
});
describe('directive: navigationButton', function() {
beforeEach(module('navigationButtonsModule'));
beforeEach(inject(function($rootScope, $compile, $state, $templateCache, $timeout) {
this.$rootScope = $rootScope;
this.$compile = $compile;
this.$state = $state;
this.$templateCache = $templateCache;
this.$timeout = $timeout;
this.testContainer = document.getElementById('test-container');
this.compileDirective = function(template, scope) {
var element = this.$compile(template)(scope);
this.testContainer.appendChild(element[0]);
scope.$digest();
return element;
}
}));
afterEach(function() {
this.testContainer.innerHTML = '';
});
it('navigation button should show passed name and ui-sref state', function() {
var template = '<navigation-button state="state" name="name"></navigation-button>';
var scope = this.$rootScope.$new();
scope.state = 'home';
scope.name = 'Home';
var element = this.compileDirective(template, scope);
expect(element.text()).toBe('Go to Home');
expect(element.find('button').attr('ui-sref')).toBe('home');
});
it('will show home href', function() {
expect(this.$state.href('home')).toEqual('#/home');
});
it('on button click browser should go to home state', function() {
var template = '<navigation-button state="state" name="name"></navigation-button>';
var scope = this.$rootScope.$new();
scope.state = 'home';
scope.name = 'Home';
var element = this.compileDirective(template, scope);
// mimicking home.html template
this.$templateCache.put('home.html', '');
this.$timeout(function() {
element.find('button')[0].click();
});
this.$timeout.flush();
this.$rootScope.$digest();
expect(this.$state.current.name).toBe('home');
});
});
<!-- jasmine -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine-html.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/boot.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.min.css" rel="stylesheet" />
<!-- angular -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular-mocks.js"></script>
<div id="test-container"></div>
JSFiddle
I have kept replicated code as it is, so it can be easy to read and understand

Resources