angular test not executing directive code - javascript

I'm having some trouble testing angular directives. Here's what I'm experiencing. I've got karma set up to run my tests. It appears that the link code within the directive (the section with console log "kicking off calculations") never actually fires. I'm not sure what I'm doing wrong. The test works as I'd expect -- except when I get down to imageNode.style -- there's nothing in there. There are no styles assigned to the image. Why is the directive code not being fired? Any ideas?
This is my module code.
(function() {
'use strict';
// sets narrower side of image to width of parent div
// used in sjwUserImage
angular.module('sjw.util', [])
.directive('imgFit', function() {
return {
restrict: 'A',
link: function(scope, element, attr) {
element.bind('load', function(e) {
console.log('kicking off calculations...');
console.log(element);
var parentWidth = element.parent().width();
if (this.naturalHeight > this.naturalWidth) {
// not horizontal or square
this.width = parentWidth;
}
else {
this.height = parentWidth;
}
});
}
};
});
})();
This is my test code.
describe('imgFit directive', function() {
var compile;
var scope;
var directiveElem;
console.log('loading sjw.util module...');
beforeEach(function(done) {
module('sjw.util', function() {
console.log('completed loading module.');
});
inject(function($compile, $rootScope) {
angular.module('sjw.util');
compile = $compile;
scope = $rootScope.$new();
directiveElem = getCompiledElement();
done();
});
});
function getCompiledElement() {
var myApp = angular.module('sjw.util');
var elem = angular.element(
'<div style="height:35px;width:35px">' +
'<img ' +
'ng-src="http://random_image_url" img-fit>' +
'</div>' +
'<div style="height:35px;width:35px">' +
'<img ' +
'ng-src="http://random_image_url" img-fit>' +
'</div>');
var compiledDirective = compile(elem)(scope);
scope.$digest();
return compiledDirective;
}
it('should assign width to parent width when image is square', function() {
var squareImageParent = directiveElem[0];
console.log(directiveElem);
expect(squareImageParent).not.toBe(null);
var imageNode = squareImageParent.childNodes[0];
expect(imageNode).toBeDefined();
console.log(imageNode);
console.log(imageNode.style);
//expect(imageNode.style.height == '35px').toBe(true);
});
});

I have exorcised this demon. My problem was with the directive itself. My images weren't finishing loading within the headless browser I'm using. So the 'load' event in 'imgFit.link' would never fire. Obviously a problem. That's why inserting my timeout (in comment above) was helping. It allowed the images to finish loading. Ugly hack, but seems like I have no way of knowing when phantom's going to load the images.
beforeEach(function(done) {
module('sjw.util');
inject(function($compile, $rootScope) {
compile = $compile;
scope = $rootScope.$new();
directiveElem = getCompiledElement();
// wait for the images to finish loading.
setTimeout(done, 2000);
});
});

Related

How to append directives based on conditions

I have 2 different directives. The first one is returns an iframe with a live video, and the second returns an iframe with a recent video.
The condition is: if live content is present, append live directive, else append recent video directive.
Ive tried with normal html instead of the directives and it works, but when i put the directive element, unfortunately doesnt work.
WORKING
controller.js
function isLiveOn() {
var liveIframe = '<h2>LIVE</h2>';
var videoIframe = '<h2>VIDEO</h2>';
if (vm.live.items[0] != undefined) {
$('.iframe-container').append(liveIframe);
} else {
$('.iframe-container').append(videoIframe);
}
};
NOT WORKING
controller.js
function isLiveOn() {
var liveIframe = '<live-iframe live="vm.live.items[0]"></live-iframe>';
var videoIframe = '<last-video video="vm.activity.items[0]"></last-video>';
if (vm.live.items[0] != undefined) {
$('.iframe-container').append(liveIframe);
} else {
$('.iframe-container').append(videoIframe);
}
};
Each directive has its own html and js file.
Something like that:
directive.html
<div class="live">
<iframe ng-src="{{getIframeSrc(live.id.videoId)}}"></iframe>
</div>
<div class="live-description">
<h4>{{live.snippet.title}}</h4>
</div>
directive.js
app.directive('live', live);
live.$inject = ['$window'];
function live($window) {
var directive = {
link: link,
restrict: 'EA',
templateUrl: 'path',
scope: {
live: '='
}
};
return directive;
function link(scope, element, attrs) {
scope.getIframeSrc = function(id) {
return 'https://www.youtube.com/embed/' + id;
};
}
}
So im thinking its some problem with the directives that im probably missing.
Any help will be appreciated!
Instead of handling the logic in the controller you can control it in UI as it will be easier.
-----Other Html Codes-----
<live-iframe ng-if="vm.live.items[0]" live="vm.live.items[0]"></live-iframe>
<last-video ng-if="!vm.live.items[0]" video="vm.activity.items[0]"></last-video>
-----Other Html Codes-----
And you can remove following lines of code from the controller
var liveIframe = '<live-iframe live="vm.live.items[0]"></live-iframe>';
var videoIframe = '<last-video video="vm.activity.items[0]"></last-video>';
if (vm.live.items[0] != undefined) {
$('.iframe-container').append(liveIframe);
} else {
$('.iframe-container').append(videoIframe);
}

Jasmine Dom element test

I'm new to writing tests and I'm trying to figure out how to test if after a click the right content is loaded. I'm testing a directive, but the loaded content in the center panel. I'm first firing a click event and then I try to compile the loaded content and check if the title is the one I expected (Test).
What I'm getting as an error is
"Cannot read property 'innerHTML' of undefined"
Any idea why and how to fix the test?
describe('someSelector', function () {
beforeEach(function () {
module(function($provide) {
$provide.decorator('$timeout', function($delegate) {
var flush = $delegate.flush;
var $timeout = jasmine.createSpy('$timeout').and.callFake($delegate);
$timeout.flush = flush;
return $timeout;
});
});
this.injectDependencies('$compile',
'$scope',
'$httpBackend',
'renderTemplateAndAppendToDom',
'renderTemplate',
'$timeout');
this.render = this.renderTemplateAndAppendToDom;
this.dirStr = '<div class="some-navigator" vx-view="history[history.currentIndex].naviView"></div>';
this.viewStr = '<center-panel></center-panel>';
});
describe('when navigating to an object', function () {
beforeEach(function () {
this.$httpBackend.resetExpectations();
});
it('should load the correct content', function () {
var dir = this.$compile(this.dirStr)(this.$scope);
dir.find('span.some-navigator-label [title="Test"]').click();
dir.remove();
var view = this.$compile(this.viewStr)(this.$scope);
var contentCheck = view.find('span.titlebar-text');
expect(contentCheck[0].innerHTML).toEqual('Test');
this.$scope.$destroy();
view.remove();
});
});
});

angular material modal and ng-file-upload

I am working on image upload using angular material, ng-file-upload, and ng-imgcrop-extended. I was previously using all of this on a normal page, and everything was working fine, but requirements have changed and I had to move this logic to a modal.
The way it works is I am using ng-imgcrop to crop the photos, and ng-file-upload does the uploading. So right now, I have an element listening to the file select, and that handles the cropping. Right now however, it is not listening to the file select, and I can only reason that is from the modal.
Here is my code
modal render
$scope.headshotModal = function(ev) {
var useFullScreen;
useFullScreen = ($mdMedia('sm') || $mdMedia('xs')) && $scope.customFullscreen;
$mdDialog.show({
locals: {
p: $scope.persona
},
controller: 'ImagesController',
templateUrl: 'application/views/images/image_modal.html',
parent: angular.element(document.body),
targetEvent: ev,
clickOutsideToClose: true,
fullscreen: useFullScreen
}).then((function(answer) {
$scope.status = 'You said the information was "' + answer + '".';
}), function() {
$scope.status = 'You cancelled the dialog.';
});
$scope.$watch((function() {
return $mdMedia('xs') || $mdMedia('sm');
}), function(wantsFullScreen) {
$scope.customFullscreen = wantsFullScreen === true;
});
};
images_controller
angular.module('App').controller('ImagesController', [
'$scope', 'p', '$mdDialog', 'ImageService', '$routeParams', function($scope, p, $mdDialog, ImageService, $routeParams) {
var handleFileSelect;
$scope.persona = p;
$scope.uploadedImg = false;
$scope.myCroppedImage = '';
$scope.myImage = '';
$scope.blockingObject = {
block: true
};
$scope.callTestFuntion = function() {
$scope.blockingObject.render(function(dataURL) {
$scope.showRender = true;
console.log('via render');
console.log(dataURL.length);
});
};
$scope.blockingObject.callback = function(dataURL) {
console.log('via function');
console.log(dataURL.length);
};
handleFileSelect = function(evt) {
var file, reader;
file = evt.currentTarget.files[0];
console.log(file);
$scope.uploadedImg = true;
reader = new FileReader;
reader.onload = function(evt) {
$scope.$apply(function($scope) {
$scope.myImage = evt.target.result;
});
};
reader.readAsDataURL(file);
};
angular.element(document.querySelector('#imgInput')).on('change', function() {
console.log('handlefileselect');
// this function runs the code needed. it is not being triggered
handleFileSelect;
});
$scope.thenRedirect = function() {
return window.location.href = "personas/" + $scope.persona.slug;
};
$scope.testCrop = function(file) {
ImageService.uploadCroppedImg(file, 'headshot', $routeParams, $scope.cropAttributes);
return $scope.thenRedirect();
};
$scope.cancel = function() {
$scope.uploadedImg = false;
return $scope.showRender = false;
};
$scope.hide = function() {
$mdDialog.hide();
};
return $scope.cancelOut = function() {
$mdDialog.cancel();
};
}
]);
modal.slim
md-dialog.fs [style="width: 100%; margin-left:25%; margin-right: 25%;" aria-label=("Image Edit") ng-cloak=""]
/form
md-toolbar.text-center
.md-toolbar-tools
h2 Image Edit
span flex=""
md-button.md-icon-button [ng-click="cancelOut()" aria-label="Cancel"]
i.fa.fa-times
md-dialog-content
.md-dialog-content
h2.text-center Edit Your Headshot
div.input-div
| Select an image file:
input#imgInput [type="file" ngf-select accept="image/*" ng-model="headshotFile"] /
/ [ng-show='uploadedImg']
div
md-button.render-btn[ng-click="callTestFuntion()"] Render
.crop-area
img-crop cropject='cropAttributes' area-type="rectangle" image="myImage" live-view="blockingObject" result-image="myCroppedImage"
a.img-upload [href="#" ngf-select="uploadBanner($file)" ngf-dimensions="$width > 149 && $height > 149"]
i.fa.fa-camera
span Banner
a.img-upload[style='cursor: pointer;'ng-click="testCrop(headshotFile)"]
i.fa.fa-upload
span Upload
a.cancel-img.img-upload [href="#" ng-click="cancel()"]
i.fa.fa-ban
span Cancel
this code works on a normal html page. But the problem seems to be it cannot listen to the angular.element(document.querySelector('#imgInput')).on('change') part of the ImagesController. does anyone know how using a modal, I can handle these types of events? I have seen that I might have to wrap some logic in the $mdDialog.show().resolve() function, but i'm not sure what it's expecting.
Any help would be appreciated :)
Based on your results, I would approach this problem by wiring up the event in the onShowing or onComplete event for the dialog. I would create a callback function here:
$mdDialog.show({
locals: {
p: $scope.persona
},
controller: 'ImagesController',
templateUrl: 'application/views/images/image_modal.html',
parent: angular.element(document.body),
targetEvent: ev,
clickOutsideToClose: true,
fullscreen: useFullScreen,
onComplete: wireUpChangeEvent,
onRemoving: removeChangeEvent // so it doesn't get wired up multiple times
})
I'm not 100%, but I think the dialog stays on the DOM after you hide(close) it. If that's the case, you either need the onRemoving event function or a check to see if it's already been wired up to prevent multiple firings. The function would be called from the controller that launches the dialog. You may need to make the two of them share the same scope by using the scope option and telling it to preserveScope. I'm also not sure if the template will be loaded and on the DOM the first time onShowing is called, so it's probably safer to use onComplete.

Unit test an AngularJS document ready function

I'm writing an AngularJS application and I'm searching for a way to unit test every single aspect.
In this particular case, I need to unit test a custom directive which I've written that represents a control.
The directive can be found here:
var officeButton = angular.module('OfficeButton', []);
officeButton.directive('officeButton', function() {
return {
restrict: 'E',
replace: false,
scope: {
isDefault: '#',
isDisabled: '#',
control: '=',
label: '#'
},
template: '<div class="button-wrapper" data-ng-click="onClick()">' +
'<a href="#" class="button normal-button">' +
'<span>{{label}}</span>' +
'</a>' +
'</div>',
controller: ['$scope', function($scope) {
var event = this;
var api = {
changeLabel: function(label) {
$scope.label = label;
},
enable: function() {
$scope.isDisabled = false;
},
disable: function() {
$scope.isDisabled = true;
},
setAsDefault: function() {
$scope.isDefault = true;
},
removeDefault: function() {
$scope.isDefault = false;
}
};
event.onClick = function() {
if (typeof $scope.control.onClick === 'function') { $scope.control.onClick(); }
};
$.extend($scope.control, api);
function Init() {
if ($scope.isDefault === 'true') { $scope.isDefault = true; }
else { $scope.isDefault = false; }
}
Init();
}],
link: function(scope, element, attributes, controller) {
scope.$watch('isDefault', function(value) {
if (value === 'true' || value) { $('a', element).addClass('button-default'); }
else { $('a', element).removeClass('button-default'); }
});
scope.onClick = function() { controller.onClick(); }
}
}
});
This directive can be called by using the following HTML snippet:
<office-button label="Office Web Controls" control="buttonController"></office-button>
Now, this directive exposes an API which functions such as changeLabel, enable, disable, ....
Now, those functions are not defined on the load of the application, meaning if at the bottom of my HTML I call the following code:
$scope.buttonController.changeLabel('Office Web Controls for Web Applications Directive Demo');
It will throw an error because the changeLabel() method is not defined.
In order to make it function, I need to wrap those calls in an angular.ready function, such as:
angular.element(document).ready(function () {
$scope.buttonController.changeLabel('Office Web Controls for Web Applications Directive Demo');
});
Here's a plunker for your information.
Now, I'm writing unit tests using Jasmine, and here's what I have for the moment:
describe('Office Web Controls for Web Applications - Button Testing.', function() {
// Provides all the required variables to perform Unit Testing against the 'button' directive.
var $scope, element;
var buttonController = {};
// Loads the directive 'OfficeButton' before every test is being executed.
beforeEach(module('OfficeButton'));
// Build the element so that it can be called.
beforeEach(inject(function($rootScope, $compile) {
// Sets the $scope variable so that it can be used in the future.
$scope = $rootScope;
$scope.control = buttonController;
element = angular.element('<office-button control="buttonController"></office-button>');
$compile(element)($scope);
$scope.$digest();
}));
it('Should expose an API with certain functions.', function() {
});
});
Now, in the it function, I would like to test if the $scope.control does expose the API as defined in the directive.
The problem is that the page needs to be ready before the API is available.
Any tought on how to change the code or how to unit test this correctly?
I've found the issue, it was just a wrong configuration on the unit test.
When using this code:
$scope.control = buttonController;
element = angular.element('<office-button control="buttonController"></office-button>');
I must change the element to:
$scope.control = buttonController;
element = angular.element('<office-button control="control"></office-button>');

How to make an AngularJS scope variable dependent on media query with EnquireJS?

The goal is to make an Angular scope variable dependent on the screen resolution. I found that the EnquireJS library does just that. The final working result should be that the menu collapses when screen is 480px, based on an isCollapsed variable. (I'm using Angular-ui from bootstrap) Note: I must use Angular.
I can attach the Angular scope variable (isCollapsed) to the Javascript $window and address the $window in EnquireJS, this is what I have.
Create module + controller and attach scope + window
angular.module('PremiumMeat', ['ui.bootstrap']);
angular.module('PremiumMeat').controller('CollapseCtrl', function ($scope, $window) {
$scope.isCollapsed = false;
$window.collapsed = false;
});
Enquire setup, initialize variable on false
// Enquire configuration
var enquireQuery= "screen and (max-width:480px)";
var enquireQueryHandler = {
setup: function () {
$scope.isCollapsed = false;
$window.collapsed = false;
angular.element(document.querySelector('[ng-controller="CollapseCtrl"]')).scope().isCollapsed=false;
},
The isCollapsed variable should be initialized on false and become true when screen size reaches 480px;
I'm trying to address the variable through the $window and both the document object, but none work.
match: function () {
$scope.isCollapsed = true;
$window.collapsed = true;
angular.element(document.querySelector('[ng-controller="CollapseCtrl"]')).scope().isCollapsed=true;
},
unmatch: function () {
$scope.isCollapsed = false;
$window.collapsed = false;
angular.element(document.querySelector('[ng-controller="CollapseCtrl"]')).scope().isCollapsed=false;
}
};
Now the question is where to initialize the enquireJS to make it all work.
enquire.register(enquireQuery, enquireQueryHandler, true);
When I put it in the controller it stops working and outside it, it doesn't overrule.
Final working result should be that the menu collapses when screen is 480px. Note: I must use Angular (angular-ui bootstrap)
I created a service to handle EnquireJS media queries in my Angular application that might be of use to you: https://gist.github.com/schuyberg/034a9982bf8326c19fc9
For your case here, the service would look something like this:
services.factory('max480', ['$rootScope', '$timeout', function ($rootScope, $timeout) {
var max480 = {
watch: function() { enquire.register('screen and (max-width: 480px)', max480handler); },
unwatch: function() { enquire.unregister('screen and (max-width: 480px)'); },
ismatch: function(callback) { $rootScope.$on('match480', callback); },
notmatch: function(callback) { $rootScope.$on('unmatch480', callback); },
};
var max480handler = {
match: function() {
$timeout(function(){
$rootScope.$emit('match480');
});
},
unmatch: function(){
$rootScope.$emit('unmatch480');
}
};
return max480;
}]);
Then, inject the service in your controller, and use the following to register the enquire listener, and act on changes:
// this setup can live in your controller
$scope.isCollapsed = false;
$window.collapsed = false;
angular.element(document.querySelector('[ng controller="CollapseCtrl"]')).scope().isCollapsed=false;
// use the service to listen for, and react to changes
max480.watch();
max480.ismatch(function(){
$scope.isCollapsed = true;
$window.collapsed = true;
angular.element(document.querySelector('[ng-controller="CollapseCtrl"]')).scope().isCollapsed=true;
});
max480.notmatch(function(){
$scope.isCollapsed = false;
$window.collapsed = false;
angular.element(document.querySelector('[ng-controller="CollapseCtrl"]')).scope().isCollapsed=false;
});
This service has the advantage of being reusable throughout your application. You can also use max480.unwatch(); to unregister the listener if you need to.

Categories