Get compiled template for controller in unit test - javascript

I have the following controller:
angular.module('app').controller('userList'
, ['$scope','appRules',function ($scope,appRules) {
$scope.isUserInRole=function(user,role){
console.log("exucuting userIsInRole:",user,role);//never happens
return appRules.userIsInRole(user,role);
}
window.sscope=$scope;
console.log($scope.menu);
}]);
This is used for the following route:
angular.module('app', [,'ui.bootstrap','ngRoute'])
.config(['$routeProvider','$locationProvider'
,function($routeProvider,$locationProvider){
$locationProvider.html5Mode(true);
$locationProvider.hashPrefix('!');
$routeProvider
.when('/admin/users',{
templateUrl:'app/js/partials/admin/users/list.html',
controller:'userList'
});
}]);
The template would have something that will check if user can see this page or not (even if someone hacks javascript it doesn't matter because when data is fetched or submitted the server checks as well).
<div data-ng-if="!isUserInRole(login.user,'Administrator')" class="red">
Please log in or log in as another user.
</div>
<div data-ng-if="isUserInRole(login.user,'Administrator')" class="green">
Page to edit users.
</div>
Now I would like to create a controller in my test and see when it renders if it renders correctly:
var controller,scope,element;
beforeEach(inject(function($controller
,$rootScope,$compile,appRules) {
scope=$rootScope;
scope.login={};
scope.isUserInRole=appRules.userIsInRole;
controller = $controller('userList',{
$scope:scope
});
//here I have the controller but how to get the html it generates
// when rendering the template?
element = $compile("<div ng-controller='userList'>"
+"<div data-ng-view=''></div></div>")(scope);
//here element isn't rendered at all, works on directives but how
// to do this with controllers?
console.log("element is:",element);
}));
I would like to write a test like
it('Check page won\'t show if user is not set or is not Administrator.'
, function() {
expect($(element).text().trim()
.indexOf("Please log in or log in as another user."))
.toBe(0,'Show login message when login.user is not set.');
});
Not sure how to get element though.
[update]
I'm actually trying to test a template, because this is used by a controller I was thinking testing it with a controller but as Jon suggested it can just be loaded as template.
How to make it compile and manipulate scope and compile again I don't know. The following:
var el = angular.element(
$templateCache
.get('app/js/partials/admin/users/list.html'));
$rootScope = _$rootScope_;
$rootScope.menu={login:{}};
el.scope($rootScope);
$rootScope.$apply();
console.log(el.text());
This gives me the output of both Please log in or log in as another user. and Page to edit users. Looks like element is just the raw element not compiled one.
Trying something like this doesn't do much either:
beforeEach(inject(function(_$compile_
, _$rootScope_,appSettings,$templateCache){
console.log("before each");
var el = angular.element($templateCache
.get('app/js/partials/admin/users/list.html'));
var compile=_$compile_;
var $rootScope=_$rootScope_;
$rootScope.login:{};
$rootScope.isUserInRole=appSettings.userIsInRole;
//I would think that compile would invoke the ng-if and appSettings.userIsInRole
// but no console log in that function is shown.
var element = compile($templateCache
.get('app/js/partials/admin/users/list.html'))($rootScope);
console.log("element is:",element.html());//=undefined
}));

Add your templates to the cache using ng-templates or ng-html2js so they are available when Karma runs. Then do this:
var scope = null;
beforeEach(inject(function($templateCache,_$compile_,_$rootScope_, _$controller_) {
//get the template from the cache
template = $templateCache.get('src/main/app/components/someController/widget-search.tpl.html');
$compile = _$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_;
scope = $rootScope.new();
//compile it
element = $compile(template)(scope);
}));
//then shove your compiled element into your controller instance
it( 'should be instantiated since it does not do much yet', function(){
ctrl = $controller('SomeController',
{
$scope: scope,
$element: element
});
scope.$digest();
// my controller should set a variable called hasInstance to true when it initializes. Here i'm testing that it does.
expect( ctrl.hasInstance).toBeTruthy();
});
For more info, see http://angular-tips.com/blog/2014/06/introduction-to-unit-test-directives/

If you are using Karma you could use the ng-html2js karma pre processor which basically runs through your templates and turns them into module which you can include in your test which populate the $templateCache so your template is available.

Related

Basic Karma Angular 1.5 Component Test

I am not sure if what I am doing is completely wrong, but when I switched from "directive" to "components" for defining a few of my HTML elements, I suddenly broke all of my Karma tests. here's what I have:
karam.conf.js
...
preprocessors: {
'module-a/module-a.view.html': ['ng-html2js'],
...,
'module-z/module-z.view.html': ['ng-html2js']
},
ngHtml2JsPreprocessor: {
moduleName: 'theTemplates'
},
...
module-a.component.js
(function(){
"use strict";
angular.module('ModuleA').component('moduleAComponent',{
controller: 'ModuleAController as moduleAVm',
templateUrl: 'module-a/module-a.view.html'
});
})();
module-a-tests.js
"use strict";
describe('ModuleA',function(){
beforeEach(module('ModuleA'));
describe('Controller',function(){
...
});
describe('Component',function(){
var element, $rootScope;
beforeEach(module('theTemplates'));
beforeEach(inject([
'$compile','$rootScope',
function($c,$rs) {
$rootScope = $rs;
element = $c('<module-a-component></module-a-component>')($rootScope);
$rootScope.$digest(); // ???
}
]));
it('should have moduleAVm',function(){
expect(element.html()).not.toBe(''); // FAILS HERE
expect(element.html()).toContain('moduleVm'); // FAILS HERE TOO
});
});
});
The Error:
Expected '' not to be ''.
OK, after reading Angular's documentation more thoroughly, I came across this statement:
The easiest way to unit-test a component controller is by using the
$componentController that is included in ngMock. The advantage of this
method is that you do not have to create any DOM elements. The
following example shows how to do this for the heroDetail component
from above.
And it dawned on me, my describe('Controller',function(){...}); was what I really needed to change, and that I should just get rid of the 'Component' portion, formally known as 'Directive'
Here's my 'Controller' now:
beforeEach(inject([
'$componentController', // replaced $controller with $componentController
function($ctrl){
ctrl = $ctrl('queryResults',{ // Use component name, instead of controller name
SomeFactory:MockSomeFactory,
SomeService:MockSomeService
});
}
]));
Now, I still get to test my controller, while simultaneously testing the component. And I no longer have to create DOM elements using $compile, $rootScope, etc.

Why is this Angular directive not compiling inside of Jasmine unit test?

Preconditions: I am using Karma to run Jasmine unit tests against my Angular.js app modules.
My app uses the following pattern to expose modules (services/directives/controllers):
simple.js
'use strict';
export default (angular) => {
angular.module('simple', [])
.directive('simple', [function() {
return {
restrict: 'E',
replace: true,
template: '<h1>COMPILED!</h1>'
};
}]);
};
The corresponding unit test for the above example looks like this:
simple.test.js
import angular from 'angular';
import mocks from 'angular-mocks';
import simpleModule from './simple';
describe('simple', () => {
var $compile,
$rootScope;
// Load the simple module, which contains the directive
beforeEach(() => {
let simpleComponent = new simpleModule(angular);
// module('simple') raises a TypeError here
// I have also tried angular.module('simple') which
// I am pretty sure is incorrect.
});
// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject((_$compile_, _$rootScope_) => {
// The injector unwraps the underscores (_) from around the
// parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('Replaces the element with the appropriate content', () => {
// Compile a piece of HTML containing the directive
var element = angular.element("<simple>not compiled</simple>");
var compiledElement = $compile(element)($rootScope);
// fire all the watches, so the scope expressions evaluate
$rootScope.$digest();
// Check that the compiled element contains the templated content
expect(element.html()).toContain("COMPILED!");
});
});
Problem: when running the above test with Karma in a web browser, the test fails and the element does not appear to be getting compiled.
What am I missing?
I can see in your code that you are missing the creation of the new $scope before do the $compile. You should do something like this:
it('Replaces the element with the appropriate content', () => {
// Compile a piece of HTML containing the directive
var scope = $rootScope.$new(); // create a new scope
var element = angular.element("<simple>not compiled</simple>");
var compiledElement = $compile(element)(scope);
// fire all the watches, so the scope expressions evaluate
scope.$digest();
// Check that the compiled element contains the templated content
expect(element.html()).toContain("COMPILED!");
});
I suspect you are not importing the directive correctly. Have you tried:
beforeEach(module('simple'));
The alternate version you indicated you tried are not correct or are patterns I have not seen:
beforeEach(() => {
let simpleComponent = new simpleModule(angular);
});
module('simple');
angular.module('simple');
The obvious is that you are using Javascript ES6 in your tests. Jasmine only understands ES5 as of now.
Try using this Jasmine ES6 Compiler, or write your tests in ES5.
Note: That your tests are not being compiled but they fail seems contradictory. They can only fail if karma attempts to run them. And they can only run if they are compiled. Are you sure about this?

Angular testing http request error

I am trying to test a http request with dynamic url
I have something in my service file like
My service file.
//other service codes..
//other service codes..
var id = $cookies.id;
return $http.get('/api/product/' + id + '/description')
//id is dynamic
Test file
describe('test', function () {
beforeEach(module('myApp'));
var $httpBackend, testCtrl, scope;
beforeEach(inject(function (_$controller_, _$httpBackend_, _$rootScope_) {
scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
testCtrl = _$controller_('testCtrl', {
$scope: scope
});
}));
it('should check the request', function() {
$httpBackend.expectGET('/api/product/12345/description').respond({name:'test'});
$httpBackend.flush();
expect(scope.product).toBeDefined();
})
});
I am getting an error saying
Error: Unexpected request: GET /api/product/description
I am not sure how to test the dynamic url. Can anyone help me about it? Thanks a lot!
You don't have id set in your code, so the url becomes:
/api/product//description
Which is reduced to what you see in the unexpected request (// -> /)
So, why isn't id defined? Show the code where you set it.
In testing 'dynamic' urls, you need to set up your test so that you know what the value of id is, and expect that. There isn't a way to expect patterns of urls.
You can modify the value of cookies by changing the first line of your describe block:
beforeEach(module('myApp', function($provide) {
$provide.value('$cookies', {
id: 3
});
}));
Now you can expect that id will be three when the URL call happens.
This is pretty crude though. You could also just inject $cookies in the second beforeEach block and set it
beforeEach(inject(function($cookies) {
$cookies.put('id', 3);
}))

Dynamically add AngularJS Script

I've got a project that's has a very rapid development cycle which causes many changes in the main app.js file. This file has the configs as well as controllers for the AngularJS app that's being used as part of the project. This created Caching issues which I tried solving as follows:
<script type="text/javascript">
var s = document.createElement('script')
s.setAttribute('src', 'assets/js/app-v2.js?v='+(new Date().getMilliseconds()));
document.body.appendChild(s);
</script>
However, this gives me an error in angular saying:
Failed to instantiate module appName due to:
Error: [$injector:nomod] Module 'appName' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
My HTML is setup as
<html ng-app="appName">....</html>
So I tried setting the ng-app dynamically after loading the script but that doesn't work either. Gives me the same issue as earlier. Is it possible for me to add the appName dynamically during or after the load of the app-v2.js file?
I have found this answer from the stackoverflow while I was facing same issue.
bootstrap() will call the AngularJS compiler for you, just like ng-app.
// Make module Foo and store $controllerProvider in a global
var controllerProvider = null;
angular.module('Foo', [], function($controllerProvider) {
controllerProvider = $controllerProvider;
});
// Bootstrap Foo
angular.bootstrap($('body'), ['Foo']);
// .. time passes ..
// Load javascript file with Ctrl controller
angular.module('Foo').controller('Ctrl', function($scope, $rootScope) {
$scope.msg = "It works! rootScope is " + $rootScope.$id +
", should be " + $('body').scope().$id;
});
// Load html file with content that uses Ctrl controller
$('<div id="ctrl" ng-controller="Ctrl" ng-bind="msg">').appendTo('body');
// Register Ctrl controller manually
// If you can reference the controller function directly, just run:
// $controllerProvider.register(controllerName, controllerFunction);
// Note: I haven't found a way to get $controllerProvider at this stage
// so I keep a reference from when I ran my module config
function registerController(moduleName, controllerName) {
// Here I cannot get the controller function directly so I
// need to loop through the module's _invokeQueue to get it
var queue = angular.module(moduleName)._invokeQueue;
for(var i=0;i<queue.length;i++) {
var call = queue[i];
if(call[0] == "$controllerProvider" &&
call[1] == "register" &&
call[2][0] == controllerName) {
controllerProvider.register(controllerName, call[2][1]);
}
}
}
registerController("Foo", "Ctrl");
// compile the new element
$('body').injector().invoke(function($compile, $rootScope) {
$compile($('#ctrl'))($rootScope);
$rootScope.$apply();
});
Hope it will help you

Testing Modal Controller: unknown provider: $modalInstanceProvider <- $modalInstance, TypeError: Attempted to assign to a readonly property

I am a bit new to AngularJs. I am using Angular UI bootstrap (0.10.0) for modal implementation. I am getting the following errors while testing a modal controller
using AngularJs 1.2.7: TypeError: Attempted to assign to a readonly property
using AngularJs 1.2.12: unknown provider: $modalInstanceProvider <- $modalInstance.
I have gone through a lot of similar questions but couldn't understand what's the problem.
As pointed in the comments by Mahery, $modalInstance is made available for injection in the controller by AngularUI Bootstrap implementation. So, we don't need any effort to "resolve" or make it available somehow.
Modal Window Issue (Unknown Provider: ModalInstanceProvider)
This is my main controller that leads to creation of modal instance on clicking open on the partial page.
var SomeCtrl = function($scope, $modal){
$scope.open = function(){
$scope.modalInstance = $modal.open({
templateUrl: '/templates/simpleModal.html',
controller: 'simpleModalController',
});
$scope.modalInstance.result.then(
function(){
console.log("clicked OK");
},
function(){
console.log("clicked Cancel");
});
};
};
someCtrl.$inject = ["$scope", "$modal"];
angular.module('angularApp').controller("someCtrl", SomeCtrl);
This is the modal controller I wish to test if it contains the necessary functions (which I intend to add later)
(function(){
var SimpleModalController = function($scope, $modalInstance){
$scope.ok = function(){
$modalInstance.close('ok');
};
$scope.cancel = function(){
$modalInstance.dismiss('cancel');
};
};
SimpleModalController.$inject = ["$scope", "$modalInstance"];
angular.module('angularApp').controller("simpleModalController", SimpleModalController);
})();
This is the test I have written for the modal controller
describe('Testing simpleModalController',function() {
var ctrlScope;
var modalInstance;
var ctrl;
beforeEach(function() {
module('angularApp');
inject(function($rootScope, $modalInstance, $controller) {
ctrlScope = $rootScope.new();
modalInstance = $modalInstance;
ctrl = $controller('simpleModalController',
{
$scope : ctrlScope,
$modalInstance : modalInstance
});
});
});
it('should check existence of scope variables and functions when created', function() {
console.log('pending test');
});
});
I have no troubles in the functionality of modal in the app, testing the main controller and integration of modal. But I am unable to test the modal controller. I think the problem is with injection of $modalInstance in the test (of simple modal controller). But as mentioned earlier, angular-ui bootstrap makes it available.
Any help is appreciated. Thanks.
Having the next Modal Controller definition:
angular.module('module').controller('ModalInstanceController',ModalInstanceController);
function ModalInstanceController($timeout, $modalInstance, $scope) {
//controller across a bunch of modals
$scope.closeModal = function(){
$modalInstance.dismiss('cancel');
};
$scope.action = function(){
$modalInstance.dismiss();
};
}
You can create an spy Object with the needed methods using jasmine, and pass that object to the controller when you create the instance:
beforeEach(inject(($controller, $timeout, $rootScope) => {
modalInstance = jasmine.createSpyObj('modalInstance', ['dismiss']);
scope = $rootScope.$new();
controller = $controller('ModalInstanceController', {
$modalInstance: modalInstance,
$scope: scope
});
}));
Later in your test scenarios you can check the spied object:
it('should defined the required methods on the scope', () => {
expect(scope.closeModal).toBeDefined();
expect(scope.action).toBeDefined();
scope.closeModal();
expect(modalInstance.dismiss).toHaveBeenCalledWith('cancel');
scope.action();
expect(modalInstance.dismiss).toHaveBeenCalledWith();
});
So.. This is one way of testing it..
describe('Testing',function() {
it('test',function() {
inject(function($rootScope, $modal) {
var fakeModal = { };
//Basically, what you want is for your modal's controller to get
//initalized and then returned to you, so the methods in it can be unit tested
spyOn(modal, 'open').andReturn(fakeModal);
ctrl = $controller('Controller',
{
$scope : ctrlScope,
$modal: modal
});
});
});
});
I've been fighting with this issue too. The problem is that the controller your trying to instantiate in your test is a total different instance the $modal service instantiates internally passing in the actual modal window as $modalInstance. See the js code comments at the example http://angular-ui.github.io/bootstrap/#/modal:
// Please note that $modalInstance represents a modal window (instance) dependency.
// It is not the same as the $modal service used above.
So how do you test your controller then? I've not found the solution yet, im sorry. The diffulty resides in that you can't have access to your controller's scope as the $modal service creates a new scope from whether is the $rootScope or a scope you pass in with your options. So you loose track.
The least you can do is test the functions you passed in to the result promise. This is done by spying on the $modal.open function and returning a mock. And this is shown here https://stackoverflow.com/a/21370703/2202143. And complement with integration tests using tools like protractor.
Take a look at the answer that was selected as correct on the question: Unit testing a modalInstance controller with Karma / Jasmine.
I was struggling with the same issue for some time, and that question (and answer) helped me test my modals (and functions to open/close them) in a really clean way!
I didn't like any answer given here so I am adding my own.
the reason I didn't like the answers above is that they don't hold once a project is a bit bigger.
for me, the solution was to simply implement an angular service called $modalInstance...
so under spec I create a folder named shims that I use for small items such as these. (make sure to add it to karma.conf)
and there I implement
angular.module(..).service('$modalInstance', function(){
this.dismiss = jasmine.createSpy('$modalInstance.dismiss');
...
});
I find this method much cleaner and much more maintainable and straightforward.
sometimes, I like making sure my shim is only loaded for specific tests, in that case I simply give it a specific module name and then I have to add a module call for it otherwise it won't load.
I also highly recommend using a different library for modals, I recommend ng-dialog for many reasons but in this context I can say it is much more test friendly, been using it for a while now.

Categories

Resources