I am unit testing an AngularJS directive with Jasmine.
I am getting this error even though I injected $compile in a beforeEach statement:
Reference Error: can't find variable: $compile
describe('test', function() {
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
describe('testCase', function() {
var nlElement = angular.element('<div directive></div>');
var element = $compile(nlElement)($rootScope); // this is where the error is being thrown
$rootScope.$digest();
it(...)
});
});
Do I have to include the statements in the second describe in the it blocks? Ultimately I want to be able to inject all three of those statements before each test, but I am trying to resolve the $compile error at the moment.
It turns out that the describe blocks are executed before the beforeEach statements, which is counter-intuitive to me. Also, if you want to initialize variables and your directive before your tests (like I did in the second describe block, then include it in a beforeEach statement, and test your assertions in it blocks.
describe('test', function() {
describe('testCase', function() {
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
beforeEach(inject(function() {
var nlElement = angular.element('<div directive></div>');
var element = $compile(nlElement)($rootScope); // this is where the error is being thrown
$rootScope.$digest();
}));
it(...)
});
});
Related
test file:
describe('$rootScope', function() {
describe('$on', function() {
var credentials = "Basic abcd1234";
var $scope;
var $rootScope;
var $httpBackend;
...
beforeEach(inject(function($injector, $controller, _$rootScope_, $state, _$q_, currentUserService) {
$scope = _$rootScope_.$new();
$rootScope = _$rootScope_;
$httpBackend.when('GET', 'dist/app/login/login.html').respond({'title': 'TEST_TITLE'}, {'A-Token': 'xxx'});
}));
...
it('should set $scope.title if noAuthorization', function() {
spyOn($rootScope, '$on');
$controller('AppCtrl', {$scope: $scope});
$rootScope.$broadcast("$stateChangeStart");
$rootScope.$apply();
expect($rootScope.$on).toHaveBeenCalled();
expect($scope.title).toBe('TEST_TITLE');
});
});
});
$scope.title is always undefined in my tests. the expect always fails. I've tried $emit, $apply, etc. This is within a controller, inside of a $rootScope.on method.
But if I console log $scope.title inside of the js file, it does show that $scope.title has bene updated.
I should also mention that the function being called is not in $scope, ie it is not $scope.updateTitle, it is just function updateTitle(...)
I don't feel the actual code is necessary to show because it does it's job. I am just wondering why the $scope in the tests is not getting updated.
In a nutshell: don't forget .andCallThrough() on the spy. The Jasmine spy needed andCallThrough(). Use element.scope() to access the correct scope.
spyOn(scope, '$apply').andCallThrough();
When using jasmine, I can't seem to test a function that has a jquery selector or document.getElementById in it. Is there a better strategy here? I normally do not put selectors in angular code, however this is a workaround.
In the function I'm testing:
this.scope.login = () => {
($('#login-form')[0] as HTMLFormElement).submit();
// or even
document.getElementById('login-form').submit(); // this is a workaround for browser auto-complete, I normally would not have selectors in angular code.
}
I get
TypeError: undefined is not an object (evaluating '$('#login-form')[0].submit')
I've tried "mocking by spying," using spyOn to try to mock the jquery selector function and return a fake element... but doesn't seem to work, or I'm not doing it right.
My spec loads the template (logs ok). Element is also defined and seems to be a valid angular-compiled element.
describe('Nav Controller Spec', function() {
beforeEach(function() {
angular.mock.module('App');
inject(function(_$controller_, $rootScope, _userModel_, _$q_, _$httpBackend_, $templateCache, _$compile_) {
scope = $rootScope.$new();
$q = _$q_;
$compile = _$compile_;
$httpBackend = _$httpBackend_;
deferred = _$q_.defer();
html = $templateCache.get('main/components/login/login.tpl.html');
var el = angular.element( html );
element = $compile( el )(scope); //element is valid and works
controller = _$controller_;
userModel = _userModel_;
scope.userModel = userModel;
// tried this... still get error
spyOn($.fn, 'val').and.returnValue('<form></form>');
//if i change to 'init' or 'find', i get 'undefined is not a constructor'
spyOn($.fn, 'init').and.returnValue('<form></form>');
ctrl = controller('loginController', { $scope: scope, $element: element });
$rootScope.$digest();
});
});
it('should login and change the status', function(){
spyOn( ctrl.scope.userModel, 'login' ).and.callThrough();
ctrl.scope.formType = 'login';
ctrl.scope.login(); //fails
expect( ctrl.scope.userModel.login ).toHaveBeenCalled();
});
As a last resort, i tried the following with document.getElementById('login-form') in the controller. However, I get TypeError: undefined is not a constructor (evaluating 'document.getElementById('login-form').submit()')
var mockElement = {
id:"login-form",
parentNode:true
};
var document_getElementById = document.getElementById;
var spy = spyOn(document, "getElementById").and.callFake(function(id){
if(id===mockElement.id){
return mockElement;
}
return document_getElementById(id);
});
Actually, this works. You will want to stub/spy with document.getElementById since that's what jquery uses under the hood. I only had forgotten to stub the submit function. I didn't realize this because jasmine's wrapped errors are so meaningless.
var mockElement = {
id:"login-form",
parentNode:true,
submit:function(){
return 'cheese';
}
};
var document_getElementById = document.getElementById;
var spy = spyOn(document, "getElementById").and.callFake(function(id){
if(id===mockElement.id){
return mockElement;
}
return document_getElementById(id);
});
I would like to know how to test some Angular scope variables at my controller that was created inside an ajax request.
What I mean is... This is my controller:
app.controller('NewQuestionCtrl', function ($scope, Question) {
loadJsonAndSetScopeVariables($scope, Question);
});
function loadJsonAndSetScopeVariables(scope, Question) {
Question.loadJson().then(function(success) {
var result = success.data.variables;
scope.levels = result.levels;
scope.tags = result.tags;
scope.difficulties = result.difficulties;
scope.questionTypes = result.questionTypes;
scope.areas = result.areas;
},function(data){
});
}
One of the prerequisites is not to use mock.
At my test I was able to inject my Question service:
describe('Controller: NewQuestionCtrl', function () {
beforeEach(angular.mock.module('testmeApp'));
var NewQuestionCtrl, scope, QuestionService;
beforeEach(inject(function ($controller, $rootScope, Question) {
scope = $rootScope.$new();
QuestionService = Question;
NewQuestionCtrl = $controller('NewQuestionCtrl', {
$scope: scope
});
}));
it('should attach a list of areas to the scope', function (done) {
expect(scope.areas).toBeDefined();
done();
});
Please, someone could help me?
Create a mock for Question and use that. There are several ways to do this. This is just one of them.
You could alternatively inject a real instance of Question and spy on that instead, but a mock is preferred to isolate these unit tests from the Question unit tests.
var questionDeferred, myController, scope;
var mockQuestion = {
loadJson: angular.noop
};
beforeEach(inject(function($q, $rootScope, $controller) {
questionDeferred = $q.defer();
scope = $rootScope.$new();
spyOn(mockQuestion, 'loadJson').and.returnValue(questionDeferred);
// Because your function is run straight away, you'll need to create
// your controller in this way in order to spy on Question.loadJson()
myController = $controller('NewQuestionCtrl', {
$scope: scope,
Question: mockQuestion
});
}));
it('should attach a list of areas to the scope', function (done) {
questionDeferred.resolve({/*some data*/});
scope.$digest();
expect(scope.areas).toBeDefined();
done();
});
I'm kinda new in AngularJS unit testing and I'm having some troubles to test a controller method that was written in a directive.
This is my directive.js
app.directive('formLogin', ['AuthService', function(AuthService){
return {
restrict: 'E',
templateUrl: utils.baseUrl + 'partials/_home-form-login.html',
replace: true,
controller: function ($scope, $element, $http, $location) {
$scope.visible = false;
$scope.showForm = function () {
$scope.visible = !$scope.visible;
};
}
};
}]);
And here goes my unit-test.js
describe('formLogin ctrl', function () {
var element, scope, compile;
beforeEach(module('Application'));
beforeEach(inject(function ($rootScope, $compile) {
element = angular.element('<form-login></form-login>');
scope = $rootScope.$new();
compile = $compile(element)($scope);
}));
it('Test', function() {
expect(scope.visible).toBe(false);
})
});
And by doing this the "scope.visible" come as undefined.
There are some way to take the $scope from my controller that assumes in "scope" variable the "visible" property and the "showForm" method?
From this link
it looks like you might need to do a scope.$digest();
You appear to have a couple problems:
compile = $compile(element)($scope); -- here, $scope is undefined. It should be compile = $compile(element)(scope);
As mentioned by smk, you need to digest your scope to finish the directive creation process
This is especially important because you are using templateUrl. When you just use a locally-defined template, as Krzysztof does in his example, you can get by with skipping this step.
You will probably notice that when you add scope.$digest() you will get a different problem about an unexpected GET request. This is because Angular is trying to GET the templateUrl, and during testing all HTTP requests must be configured / expected manually. You might be tempted to inject $httpBackend and do something like $httpBackend.whenGet(/partials\//).respond('<div/>'); but you will end up with problems that way.
The better way to accomplish this is to inject the template $templateCache -- Karma has a pre-processor to do this for you, or you can do it manually. There have been other StackOverflow questions you can read about this, like this one.
I've modified your example to manually insert a simple template into the $templateCache as a simple example in this plunkr.
You should take a look into Karma's html2js pre-processor to see if it can do the job for you.
If your directive hasn't isolated scope, you can call methods from directive controller and test how it impacts to scope values.
describe('myApp', function () {
var scope
, element
;
beforeEach(function () {
module('myApp');
});
describe('Directive: formLogin', function () {
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
element = angular.element('<form-login></form-login>');
$compile(element)(scope);
}));
it('.showForm() – changes value of $scope.visible', function() {
expect(scope.visible).toBe(false);
scope.showForm();
expect(scope.visible).toBe(true);
});
});
});
jsfiddle: http://jsfiddle.net/krzysztof_safjanowski/L2rBV/1/
Case
When I create a spy on rootScope, the expectation fails for some reason. Check out the plunkr and try just commenting it out and reversing to see.
Code
Plunker Example
describe('Testing', function() {
var rootScope = null
//you need to indicate your module in a test
// beforeEach(module('plunker'));
beforeEach(inject(function($rootScope, $controller) {
rootScope = $rootScope;
rootScope.value = false;
rootScope.testFn = function() {
rootScope.value = true;
}
}));
it('should modify root scope', function() {
// Creating this spy makes test fail
// Comment it outto make it pass
spyOn(rootScope, 'testFn');
expect(rootScope.value).toEqual(false);
rootScope.testFn();
expect(rootScope.value).toEqual(true);
});
});
You need to tell the spy to do something:
spyOn(rootScope, 'testFn').andCallThrough();
I updated the plnkr here: http://plnkr.co/edit/t3ksMtKSI3CEkCtpZ8tI?p=preview
Hope this helped!