I have the following Angular code...
<!-- Directive Template -->
<div style="display:hidden" ng-if="ctrl.test()">
...
// Controller
...
this.test = function() {
return false;
}
Now I need to mock ctrl.test so I can test that the DOM is rendered properly when true (this isn't the real code just for demo that is why it returns false). I try the following test...
beforeEach(module('mod'));
var $controller, $scope, $httpBackend, $rootScope, $compile;
beforeEach(inject(function(_$controller_, _$rootScope_, _$httpBackend_, _$compile_){
$rootScope = _$rootScope_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
$controller = _$controller_(ImpactFormController, {
$scope: $scope
});
$compile = _$compile_;
}));
describe("Form Functionality", function(){
it("Submitting saves the form properly", function() {
//TODO Should we check the response this seems uness.
var compiled = $compile('<my-d></my-d>')($rootScope);
$rootScope.$digest();
var controller = compiled.controller('my-d');
spyOn(controller, "test").and.returnValue(true);
$rootScope.$digest();
console.log(JSON.stringify(controller.test()));
console.log(JSON.stringify(compiled[0].innerHTML));
});
})
The console that is calling the method returns true as expected. However, the DOM is never updated. After the mock I would expect something like this...
<div class="wrapper">
<div style="display:hidden"> </div> <!-- ng-if -->
</div>
I also tried...
// Trying to recompile
spyOn(controller, "test").and.returnValue(true);
$rootScope.$digest();
var recompiled = $compile(elem)($rootScope);
console.log(JSON.stringify(recompiled[0].innerHTML));
Still nothing.
Related
I'm trying to test one of my directives and I can't work out why my scope object is undefined.
define(["require", "exports", 'angular', 'directives', "angularMocks", "chartApp"], function (require, exports, angular, Directives) {
"use strict";
describe('board controls', function () {
describe('task filter', function () {
var $compile;
var $rootScope;
var scope;
var element;
beforeEach(angular.mock.module('chartApp'));
beforeEach(angular.mock.module('partials/directives/board-controls.html'));
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
expect(scope).toBeDefined();
element = $compile('<board-controls></board-controls>')(scope);
scope.$digest();
}));
it('displays modal', function () {
scope.showChildFilters();
});
});
});
});
In the it('displays modal')... part Karma outputs:
TypeError: undefined is not an object (evaluating 'scope.showChildFilters')
But in the beforeEach(...) part it seems to be working. I just can't see why this doesn't work.
You need to change to this
it('displays modal', function () {
//scope.showChildFilters();
var isolateScope = element.isolateScope(); //I prefer to name isolateScope
isolateScope.$apply() //cause scope to digest and watch and all that
isolateScope.showChildFilters();
});
This is a very detailed answer to your question as well.
Testing element directive - can't access isolated scope methods during tests
I have a nested controller as below:
<div ng-controller="ParentController">{{ data.value }}
<div ng-controller="ChildController">{{ data.value }}</div>
</div>
app.controller('ParentController', function ($scope) {
$scope.data = {
value: "A"
}
});
My child controller sets the parent scope as below:
app.controller('ChildController', function ($scope) {
$scope.data.value = "B";
});
My Jasmine unit test is:
describe('ChildController', function () {
var scope, $rootScope;
beforeEach(inject(function ($controller, _$rootScope_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
$controller('ChildController', { $scope: scope});
scope.$digest();
}));
it('should change parent scope', function () {
expect(scope.data.value).toEqual("B");
});
});
The test results in "Cannot read property 'value' of undefined".
How do I unit test a child controller that uses a parent scope?
It really depends on what you want to test. If you want to assert that the child controller changes the value during its initialization then just setup the value for the test to change. You don't need to test Angular's scope hierarchy.
So, I'd suggest just to do this:
describe('ChildController', function () {
var scope, $rootScope;
beforeEach(inject(function ($controller, _$rootScope_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
//setup
scope.data = { value: "A" };
$controller('ChildController', { $scope: scope});
scope.$digest();
}));
it('should change parent scope', function () {
expect(scope.data.value).toEqual("B");
});
});
First initialize $scope.data={};
app.controller('ChildController', function ($scope) {
$scope.data={};
$scope.data.value = "B";
});
After giving up on ngMockE2E being able to supply proper passThrough() from the $httpBackend, I've resorted to adding the templates to the cache manually. This seems to work great, if I want to just 'test' my template. However, say I want to test some conditions on the controller that get invoked (via directive), how do a I get a reference to it?
describe('RedactedFilter ', function() {
var ctrl, filterModel,createController;
var $httpBackend,$scope,template, $templateCache,$compile,$rootScope, $compile;
beforeEach(module('RedactedApp.Services'));
beforeEach(module('RedactedApp.Controllers'));
beforeEach(module('RedactedApp.Models'));
beforeEach(inject(function($templateCache,_$compile_,_$rootScope_) {
//assign the template to the expected url called by the directive and put it in the cache
template = $templateCache.get('src/main/app/components/redactedFilter/redacted-filter.tpl.html');
$templateCache.put('src/main/app/components/redactedFilter/redactedFilter-filter.tpl.html',template);
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
beforeEach(inject(function ($controller, $rootScope,_$q_, $injector, _$compile_) {
$scope = $rootScope.$new();
$httpBackend = $injector.get('$httpBackend');
$compile = _$compile_;
filterModel = $injector.get('RedactedFilterModel');
}));
it('should default to tab 0', function() {
var formElement = angular.element('<redacted-filter></redacted-filter>');
var element = $compile(formElement)($rootScope);
$rootScope.$digest();
var ctrl = element.controller();
//ctrl is undefined
expect(ctrl.selectedTab).toEqual(0);
});
});
Note that it does not say function controller() is undefined, but the result of calling it. My directive has replace set to false so I don't think it's an issue with the transclusion hiding the element.
Here's my directive for good measure:
angular.module('RedactedApp.Directives').directive('redactedFilter', function(){
return {
restrict: 'E',
replace: false,
templateUrl: '../../main/app/components/redactedFilter/redacted-filter.tpl.html'
};
});
First, use ng-html2j or ng-templates to shove your templates into your template cache before you run tests.
Then compile the templates into elements, and shove them into your controller for testing:
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_;
//compile it
element = $compile(template)($rootScope);
}));
//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: $rootScope.$new(),
$element: element
});
expect( ctrl.hasInstance).toBeTruthy();
});
I have a directive that I want to unittest, but I'm running into the issue that I can't access my isolated scope. Here's the directive:
<my-directive></my-directive>
And the code behind it:
angular.module('demoApp.directives').directive('myDirective', function($log) {
return {
restrict: 'E',
templateUrl: 'views/directives/my-directive.html',
scope: {},
link: function($scope, iElement, iAttrs) {
$scope.save = function() {
$log.log('Save data');
};
}
};
});
And here's my unittest:
describe('Directive: myDirective', function() {
var $compile, $scope, $log;
beforeEach(function() {
// Load template using a Karma preprocessor (http://tylerhenkel.com/how-to-test-directives-that-use-templateurl/)
module('views/directives/my-directive.html');
module('demoApp.directives');
inject(function(_$compile_, _$rootScope_, _$log_) {
$compile = _$compile_;
$scope = _$rootScope_.$new();
$log = _$log_;
spyOn($log, 'log');
});
});
it('should work', function() {
var el = $compile('<my-directive></my-directive>')($scope);
console.log('Isolated scope:', el.isolateScope());
el.isolateScope().save();
expect($log.log).toHaveBeenCalled();
});
});
But when I print out the isolated scope, it results in undefined. What really confuses me though, if instead of the templateUrl I simply use template in my directive, then everything works: isolateScope() has a completely scope object as its return value and everything is great. Yet, somehow, when using templateUrl it breaks. Is this a bug in ng-mocks or in the Karma preprocessor?
Thanks in advance.
I had the same problem. It seems that when calling $compile(element)($scope) in conjunction with using a templateUrl, the digest cycle isn't automatically started. So, you need to set it off manually:
it('should work', function() {
var el = $compile('<my-directive></my-directive>')($scope);
$scope.$digest(); // Ensure changes are propagated
console.log('Isolated scope:', el.isolateScope());
el.isolateScope().save();
expect($log.log).toHaveBeenCalled();
});
I'm not sure why the $compile function doesn't do this for you, but it must be some idiosyncracy with the way that templateUrl works, as you don't need to make the call to $scope.$digest() if you use an inline template.
With Angularjs 1.3, if you disable debugInfoEnabled in the app config:
$compileProvider.debugInfoEnabled(false);
isolateScope() returns undefined also!
I had to mock and flush the $httpBackend before isolateScope() became defined. Note that $scope.$digest() made no difference.
Directive:
app.directive('deliverableList', function () {
return {
templateUrl: 'app/directives/deliverable-list-directive.tpl.html',
controller: 'deliverableListDirectiveController',
restrict = 'E',
scope = {
deliverables: '=',
label: '#'
}
}
})
test:
it('should be defined', inject(function ($rootScope, $compile, $httpBackend) {
var scope = $rootScope.$new();
$httpBackend.expectGET('app/directives/deliverable-list-directive.tpl.html').respond();
var $element = $compile('<deliverable-list label="test" deliverables="[{id: 123}]"></deliverable-list>')(scope);
$httpBackend.flush();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
expect($element).toBeDefined();
expect($element.controller).toBeDefined();
scope = $element.isolateScope();
expect(scope).toBeDefined();
expect(scope.label).toEqual('test');
expect(scope.deliverables instanceof Array).toEqual(true);
expect(scope.deliverables.length).toEqual(1);
expect(scope.deliverables[0]).toEqual({id: 123});
}));
I'm using Angular 1.3.
You could configure karma-ng-html2js-preprocessor plugin. It will convert the HTML templates into a javascript string and put it into Angular's $templateCache service.
After set a moduleName in the configuration you can declare the module in your tests and then all your production templates will available without need to mock them with $httpBackend everywhere.
beforeEach(module('partials'));
You can find how to setup the plugin here: http://untangled.io/how-to-unit-test-a-directive-with-templateurl/
In my case, I kept running into this in cases where I was trying to isolate a scope on a directive with no isolate scope property.
function testDirective() {
return {
restrict:'EA',
template:'<span>{{ message }}</span>'
scope:{} // <-- Removing this made an obvious difference
};
}
function testWithoutIsolateScopeDirective() {
return {
restrict:'EA',
template:'<span>{{ message }}</span>'
};
}
describe('tests pass', function(){
var compiledElement, isolatedScope, $scope;
beforeEach(module('test'));
beforeEach(inject(function ($compile, $rootScope){
$scope = $rootScope.$new();
compiledElement = $compile(angular.element('<div test-directive></div>'))($scope);
isolatedScope = compiledElement.isolateScope();
}));
it('element should compile', function () {
expect(compiledElement).toBeDefined();
});
it('scope should isolate', function () {
expect(isolatedScope).toBeDefined();
});
});
describe('last test fails', function(){
var compiledElement, isolatedScope, $scope;
beforeEach(module('test'));
beforeEach(inject(function ($compile, $rootScope){
$scope = $rootScope.$new();
compiledElement = $compile(angular.element('<div test-without-isolate-scope-directive></div>'))($scope);
isolatedScope = compiledElement.isolateScope();
}));
it('element should compile', function () {
expect(compiledElement).toBeDefined();
});
it('scope should isolate', function () {
expect(isolatedScope).toBeDefined();
});
});
I have some JavaScript written in the context of AngularJS. My relevant JavaScript looks like the following:
.factory('$myFactory', function ($myLibrary, $interpolate) {
return {
myFunction: function ($scope, $attrs, p) {
if (p !== null) {
$attrs.set('myProperty', p);
}
}
};
I am trying to unit test this code. In an attempt to unit test the code, I'm using Jasmine.
it('should set myProperty', inject(function ($scope, $myFactory) {
// $myFactory.myFunction($scope
}));
I can't figure out how to inject some $attrs from my unit test. How do I do that? I can successfully get my $scope setup. However, I don't understand how to inject $attrs. Is there something special about it that I'm not aware of? I'm having a similar issue with $element, though that one is out of the context of this specific test.
Thank you!
Here is a plunker: http://plnkr.co/edit/k1cxSjpAXhhJUEOcx9PG
Maybe there is a better solution but That's what I got.
$scope is easy to get, you can inject $rootScope everywhere
$attrs on the other hand is only available through the $compile variable (it lives in compile.js)
My solution is to create a fake controller , to compile it and to hijack it's $attrs.
So that's how it looks like:
var injected = null
function fakeController($scope, $attrs, $element){
injected = {}
injected.$scope = $scope;
injected.$attrs = $attrs;
injected.$element = $element;
}
describe('Testing a Hello World controller', function() {
var $scope = null;
var $attrs = null;
var $element = null;
var ctrl = null;
//you need to indicate your module in a test
beforeEach(module('plunker'));
beforeEach(inject(function($compile, $rootScope, $controller) {
$compile('<span ng-controller="fakeController"></span>')($rootScope);
$scope = injected.$scope;
$attrs = injected.$attrs;
$element = injected.$element;
ctrl = $controller('MainCtrl', {
$scope: $scope,
$attrs: $attrs,
$element: $element
});
}));
it('should say hallo to the World', function() {
expect($scope.name).toEqual('World');
});
});
seems that you can just instantiate the controller with empty object, as well...
ctrl = $controller('MyCtrl', {
$scope: $scope,
$attrs: {}
});
In latest versions of AngularJS a controller needs to be provided to get full $attrs.
See $controllerProvider documentation.
Here's a snippet
describe('angular-component-controller', function() {
// save injected parameters of controller
var injected = {};
var controller;
// #see $https://docs.angularjs.org/api/ngMock/service/$componentController
// we need to extract those from $compile instead of use as locals
angular.module('locals', []).controller('controller',
['$attrs', function($attrs) {
injected.$attrs = $attrs;
}]
);
beforeEach(module('locals'));
beforeEach(inject(function($rootScope, $compile, $componentController) {
// invoke dummy component to get $attrs
$compile('<span ng-controller="controller">')($rootScope);
var locals = {};
locals.$scope = $rootScope.$new();
locals.$attrs = injected.$attrs;
var bindings = {};
controller = $componentController('component', locals, bindings);
}));
});
See this gist AngularJS $componentController unit test