I have several protractor / angularjs it blocks that repeat the same bit of code that I would like to put inside a function. I want to just call the function instead of repeating this over and over.
it('should move directly to Draft', function() {
posting_sum_page.locate_action_button.click();
posting_action_page.move_action.filter(function(elem) {
return elem.getText().then(function(text) {
return text === 'Draft';
});
}).click();
});
This part of the block is the repeated part I want to create a function for. I am new to javascript so this is eluding me on how to do this.
return elem.getText().then(function(text) {
return text === 'Draft';
});
}).click();
I need to be able to substitute 'Draft' with different variables. I am using page objects for part of this and I am not sure A) how to create a function like this and pass in my text & B) if it should go on the spec side or the page side? This is probably pretty basic for most folks. But since I am new to javascript I am having trouble wrapping my head around this.
I would extract the whole filter function into a "helpers" module.
helpers.js:
var Helpers = function () {
this.filterByText = function (text) {
return function (elem) {
return elem.getText().then(function(actualText) {
return actualText === text;
});
};
}
}
module.exports = new Helpers();
Usage in the test:
var helpers = require("helpers");
describe("My Test", function () {
it('should move directly to Draft', function() {
posting_sum_page.locate_action_button.click();
posting_action_page.move_action.filter(helpers.filterByText('Draft')).click();
});
});
Maybe something like this?
describe('...something...', function()
{
var clickBtn;
beforeEach(function()
{
clickBtn = function(testText)
{
return posting_action_page.move_action.filter(function(elem)
{
return elem.getText().then(function(currentText)
{
return currentText === testText;
});
}).click();
};
});
it('should move directly to Draft', function()
{
posting_sum_page.locate_action_button.click();
expect(clickBtn('Draft')).toEqual('...something...');
});
});
If you want to reuse the return block only
it('should move directly to' + targetText, function() {
posting_sum_page.locate_action_button.click();
posting_action_page.move_action.filter(function(elem) {
checkSameText(elem, targetText);
}).click();
});
function checkSameText(el, targetText) {
return el.getText().then(function(text) {
return text === targetText;
});
}
I'm unsure what the page object type is for posting_action_page.move_action but what I think you are looking for is using by.buttonText or by.linkText.
// html: <button>Draft</button>
element(by.buttonText('Draft')).click();
// html: <a href="">Draft</button>
element(by.linkText('Draft')).click();
There are other locators that might be helpful like by.partialButtonText and by.partialLinkText.
Related
I have unpredictable behavior page that depends on changes that create developers. And sometimes my tests failed, because page wasn't load. My test scenario structure looks like:
describe('0. first actions', function () {
var lib = require("../../common.js");
var config = browser.params;
var url = config.listOfReferencesUrl, toolbar;
load(url, "list-of-references");
beforeAll(function () {
// some actions on the page
});
it('test0', function () {
since('test0 failed').
expect(toolbar.isPresent()).toBe(true);
});
describe('1.actions1', function () {
beforeAll(function () {
// some actions on the page
});
it('test1', function () {
since('test1 failed').
expect(table.getRow(clientNameNum).getRowInput().isEnabled()).toBe(true);
});
// ... another invested describes
});
Where load function is:
global.load = function (url, pageType) {
browser.get(url);
if (pageType == 'list-of-references'){
browser.executeScript("icms.go('WEB_INQ_PROC', 'InquiryList', null, 0)");
}
browser.waitForAngular();
};
I wonder if I can create structure to stop my tests if page isn't load. But I don't want to use 'jasmine-bail-fast', because I want to see another failures if page will load.
I tried to write something like:
if (this.results_.failedCount > 0) {
// Hack: Quit by filtering upcoming tests
this.env.specFilter = function(spec) {
return false;
};
}
But it isn't working. I use jasmine2.
Maybe somebody know how I can organize it?
You can define a wrapper
var filter = function (fn) {
if (!condition)
throw new Error('skipped');
return fn;
}
and use it on all relevant describe/it blocks:
describe('...', filter(function () {
...
}));
Before asking my real question, I have a different one... Does it make sense to unit test DOM manipulation in Angular directives?
For instance, here's my complete linking function:
function linkFn(scope, element) {
var ribbon = element[0];
var nav = ribbon.children[0];
scope.ctrl.ribbonItemClick = function (index) {
var itemOffsetLeft;
var itemOffsetRight;
var item;
if (scope.ctrl.model.selectedIndex === index) {
return;
}
scope.ctrl.model.selectedIndex = index;
item = nav.querySelectorAll('.item')[index];
itemOffsetLeft = item.offsetLeft - ribbon.offsetLeft;
itemOffsetRight = itemOffsetLeft + item.clientWidth;
if (itemOffsetLeft < nav.scrollLeft) {
nav.scrollLeft = itemOffsetLeft - MAGIC_PADDING;
}
if(itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
nav.scrollLeft = itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
}
this.itemClick({
item: scope.ctrl.model.items[index],
index: index
});
$location.path(scope.ctrl.model.items[index].href);
};
$timeout(function $timeout() {
var item = nav.querySelector('.item.selected');
nav.scrollLeft = item.offsetLeft - ribbon.offsetLeft - MAGIC_PADDING;
});
}
This is for a scrollable tabbed component and I have no idea how to test the 3 instances of nav.scrollLeft = x.
The first two if statements happen when an item - which is only partially visible - is clicked. The left/right (each if) item will be snapped to the left/right border of the component.
The third one, is to place the selected item in view if it's not visible when the component is loaded.
How do I unit test this with Karma/Jasmine. does it even make sense to do it or should I do functional tests with Protractor instead?
When testing directives, look for things that set or return explicit values. These are generally easy to assert and it makes sense to unit test them with Jasmine and Karma.
Take a look at Angular's tests for ng-src. Here, they test that the directive works by asserting that the src attribute on the element gets set to the right values. It is explicit: either the src attribute has a specific value or it does not.
it('should not result empty string in img src', inject(function($rootScope, $compile) {
$rootScope.image = {};
element = $compile('<img ng-src="{{image.url}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('src')).not.toBe('');
expect(element.attr('src')).toBe(undefined);
}));
The same with ng-bind. Here, they pass a string of HTML with to the $compiler and then assert that the return value has had its HTML populated with actual scope values. Again, it is explicit.
it('should set text', inject(function($rootScope, $compile) {
element = $compile('<div ng-bind="a"></div>')($rootScope);
expect(element.text()).toEqual('');
$rootScope.a = 'misko';
$rootScope.$digest();
expect(element.hasClass('ng-binding')).toEqual(true);
expect(element.text()).toEqual('misko');
}));
When you get into more complicated scenarios like testing against viewport visibility or testing whether specific elements are positioned in the right places on the page, you could try to test that CSS and style attributes get set properly, but that gets fiddly real quick and is not recommended. At this point you should be looking at Protractor or a similar e2e testing tool.
I would 100% want to test all the paths of your directive even if it isn't the easiest thing. But there are approaches you can take to make this process simpler.
Break Complicated Logic into Service
The first thing that stands out to me is the complicated piece of logic about setting the nav scrollLeft. Why not break this into a separate service that can be unit tested on its own?
app.factory('AutoNavScroller', function() {
var MAGIC_PADDING;
MAGIC_PADDING = 25;
return function(extraOffsetLeft) {
this.getScrollPosition = function(item, nav) {
var itemOffsetLeft, itemOffsetRight;
itemOffsetLeft = item.offsetLeft - extraOffsetLeft;
itemOffsetRight = itemOffsetLeft + item.clientWidth;
if ( !!nav && itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
return itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
} else {
return itemOffsetLeft - MAGIC_PADDING;
}
};
}
});
This makes it much easier to test all the paths and refactor (which you can see I was able to do above. The tests can be seen below:
describe('AutoNavScroller', function() {
var AutoNavScroller;
beforeEach(module('app'));
beforeEach(inject(function(_AutoNavScroller_) {
AutoNavScroller = _AutoNavScroller_;
}));
describe('#getScrollPosition', function() {
var scroller, item;
function getScrollPosition(nav) {
return scroller.getScrollPosition(item, nav);
}
beforeEach(function() {
scroller = new AutoNavScroller(50);
item = {
offsetLeft: 100
};
})
describe('with setting initial position', function() {
it('gets the initial scroll position', function() {
expect(getScrollPosition()).toEqual(25);
});
});
describe('with item offset left of the nav scroll left', function() {
it('gets the scroll position', function() {
expect(getScrollPosition({
scrollLeft: 100
})).toEqual(25);
});
});
describe('with item offset right of the nav width and scroll left', function() {
beforeEach(function() {
item.clientWidth = 300;
});
it('gets the scroll position', function() {
expect(getScrollPosition({
scrollLeft: 25,
clientWidth: 50
})).toEqual(325);
});
});
});
});
Test That Directive is Calling Service
Now that we've broken up our directive, we can just inject the service and make sure it is called correctly.
app.directive('ribbonNav', function(AutoNavScroller, $timeout) {
return {
link: function(scope, element) {
var navScroller;
var ribbon = element[0];
var nav = ribbon.children[0];
// Assuming ribbon offsetLeft remains the same
navScroller = new AutoNavScroller(ribbon.offsetLeft);
scope.ctrl.ribbonItemClick = function (index) {
if (scope.ctrl.model.selectedIndex === index) {
return;
}
scope.ctrl.model.selectedIndex = index;
item = nav.querySelectorAll('.item')[index];
nav.scrollLeft = navScroller.getScrollLeft(item, nav);
// ...rest of directive
};
$timeout(function $timeout() {
var item = nav.querySelector('.item.selected');
// Sets initial nav scroll left
nav.scrollLeft = navScroller.getScrollLeft(item);
});
}
}
});
The easiest way to make sure our directive keeps using the service, is to just spy on the methods that it will call and make sure they are receiving the correct parameters:
describe('ribbonNav', function() {
var $compile, $el, $scope, AutoNavScroller;
function createRibbonNav() {
$el = $compile($el)($scope);
angular.element(document)
$scope.$digest();
document.body.appendChild($el[0]);
}
beforeEach(module('app'));
beforeEach(module(function ($provide) {
AutoNavScroller = jasmine.createSpy();
AutoNavScroller.prototype.getScrollLeft = function(item, nav) {
return !nav ? 50 : 100;
};
spyOn(AutoNavScroller.prototype, 'getScrollLeft').and.callThrough();
$provide.provider('AutoNavScroller', function () {
this.$get = function () {
return AutoNavScroller;
}
});
}));
beforeEach(inject(function(_$compile_, $rootScope) {
$compile = _$compile_;
$el = "<div id='ribbon_nav' ribbon-nav><div style='width:50px;overflow:scroll;float:left;'><div class='item selected' style='height:100px;width:200px;float:left;'>An Item</div><div class='item' style='height:100px;width:200px;float:left;'>An Item</div></div></div>";
$scope = $rootScope.$new()
$scope.ctrl = {
model: {
selectedIndex: 0
}
};
createRibbonNav();
}));
afterEach(function() {
document.getElementById('ribbon_nav').remove();
});
describe('on link', function() {
it('calls AutoNavScroller with selected item', inject(function($timeout) {
expect(AutoNavScroller).toHaveBeenCalledWith(0);
}));
it('calls AutoNavScroller with selected item', inject(function($timeout) {
$timeout.flush();
expect(AutoNavScroller.prototype.getScrollLeft)
.toHaveBeenCalledWith($el[0].children[0].children[0]);
}));
it('sets the initial nav scrollLeft', inject(function($timeout) {
$timeout.flush();
expect($el[0].children[0].scrollLeft).toEqual(50);
}));
});
describe('ribbonItemClick', function() {
beforeEach(function() {
$scope.ctrl.ribbonItemClick(1);
});
it('calls AutoNavScroller with item', inject(function($timeout) {
expect(AutoNavScroller.prototype.getScrollLeft)
.toHaveBeenCalledWith($el[0].children[0].children[1], $el[0].children[0]);
}));
it('sets the nav scrollLeft', function() {
expect($el[0].children[0].scrollLeft).toEqual(100);
});
});
});
Now, obviously these specs can be refactored a 100 ways but you can see that a higher coverage is much easier to achieve once we started breaking out the complicated logic. There are some risks around mocking objects too much because it can make your tests brittle but I believe the tradeoff is worth it here. Plus I can definitely see that AutoNavScroller being generalized and reused elsewhere. That would not have been possible if the code existed in the directive before.
Conclusion
Anyways, the reason why I believe Angular is great is the ability to test these directives and how they interact with the DOM. These jasmine specs can be run in any browser and will quickly surface inconsistencies or regressions.
Also, here is a plunkr so you can see all the moving pieces and experiment: http://plnkr.co/edit/wvj4TmmJtxTG0KW7v9rn?p=preview
I am not very familiar with the CKEDITOR API yet and now I got stuck trying to find the way to create placeholders inside of the CKEDITOR editable area.The expected behaviour for the placeholder - to dissappear on user interaction with it, allowing to edit the content instead.
I know that there is already a placeholder plugin (http://ckeditor.com/addon/placeholder) but its behaviour is not what I am looking for.
To be more specific, the question is: is it possible to subscribe for some events on the particular element inside of the CKEDITOR?
Working in the angular context I am able to compile my html before it is passed to the CKEDITOR ng-model
$scope.html = $compile('<div><span text-placeholder >Placeholder</span></div>')($scope).html();
But then I fail trying to set click events inside of the directive:
.directive('textPlaceholder', [ function () {
return {
restrict: 'A',
link: function ($scope, $element) {
//THIS DOES NOT WORK UNFORTUNATELY
$element.on('click', function () {
console.log('clicked');
})
}
}
}])
Any thoughts?
UPDATE: For now I came up with the solution to implement simple plugin and then reference it in the CKEDITOR config:
(function () {
CKEDITOR.plugins.add('text-placeholder', {
init: function (editor) {
editor.on('key', function (evt) {
var el = $(CKEDITOR.instances.editor1.getSelection().getNative().baseNode.parentElement);
if (el.hasClass('text-placeholder')) {
el.remove();
}
});
}
});
})();
Looks ugly for me. Any feedback is appreciated.
This seems to be a final Solution:
CKEDITOR.plugins.add('text-placeholder', {
init: function (editor) {
editor.on('contentDom', function () {
var editable = editor.editable();
editable.attachListener(editable, 'click', function (event) {
var $placeholder = $(event.data.$.target).closest('.text-placeholder');
if ($placeholder.length > 0) {
var selection = editor.getSelection();
selection.selectElement(selection.getStartElement());
}
});
});
}
});
This applies the selection on the element with "text-placeholder" class when user focuses it inside of the editable area
Update:
See example
You inspired me to write one myself, using the above example as a starting point. In my use case I wanted to take placeholder text from an attribute on the editor -- data-placeholder -- and display it in the editor. When the editor gets focus, the placeholder text disappears. When the editor blurs -- if no user content has been entered -- the placeholder text is displayed again. Additionally, I set a data-placeholder-showing attribute so that I can, for example, use CSS to make the placeholder text gray. Here's my code:
CKEDITOR.plugins.add('text-placeholder', {
init: function (editor) {
var placeholder = editor.element.getAttribute('data-placeholder');
editor.on('contentDom', function () {
if (placeholder) {
editor.setData(placeholder);
editor.element.setAttribute('data-placeholder-showing', true);
}
});
editor.on('focus', function() {
if (editor.getData() === placeholder) {
editor.element.setAttribute('data-placeholder-showing', false);
editor.setData('');
}
});
editor.on('blur', function() {
if (placeholder && editor.getData().length === 0) {
editor.element.setAttribute('data-placeholder-showing', true);
editor.setData(placeholder);
}
});
}
});
I have this controller that has several functions that call each other. On success, I want to return something to be displayed (located in the last function). For some reason, without errors, the return is not working but the console.log is. Can someone please tell me why the return does not work and give me a solution please. Thanks so much!
.controller("dayController", function(){
.controller("weatherController", function(){
this.currentWeatherToDisplay = function(){
if(navigator.geolocation){
navigator.geolocation.getCurrentPosition(gotLocation,initialize);
}
else{
alert("Device does not support geolocation");
initialize();
}
};
var currentLocation;
//get the location coords
function gotLocation(pos){
var crd = pos.coords;
currentLocation = loadWeather(crd.latitude+','+crd.longitude);
initialize();
}
function initialize(){
if(!currentLocation){
loadWeather("Washington, DC");
}
else{
loadWeather(currentLocation);
}
}
function loadWeather(location){
$.simpleWeather({
location: location,
woeid: '',
unit: 'f',
success: function(weather) {
var html = weather.temp+'°'+weather.units.temp;
console.log(html);
return html;
},
error: function(error) {
console.log(error);
return error;
}
});
}
});
Well, mmmm you are use some jQuery plugin to get the weather given a current location, and like almost every jQuery plugins this use callbacks call to works (success, and error) first one i recommend you to rewrite this method to something like this:
function loadWeather(location){
var defered = $q.defer();
$.simpleWeather({
location: location,
woeid: '',
unit: 'f',
success: function(weather) {
var html = weather.temp+'°'+weather.units.temp;
console.log(html);
defered.resolve(html);
},
error: function(error) {
console.log(error);
defered.reject(error);
}
});
return defered.promise;
}
Also you must inject the $q dependency to the controller, like this:
module.controller("weatherController", function($q){...}
or this
module.controller("weatherController", ['$q',function($q){...}
I recommend the last by minyfication improvement in angular, when you return a promise like the function loadWeather, you must understand some basic principles about the the $q (based kriskoval Q library), a promise is a expected value in the future, an have a method then to work with that data (its a very short concept), that means:
function gotLocation(pos){
var crd = pos.coords;
loadWeather(crd.latitude+','+crd.longitude)
.then(function(html){
//html contain the expected html from loadWeather defered.resolve(html)
currentLocation = html;
})
.catch(function(error){
//error contain the expected error by execute defered.reject(error)
// maybe gonna try to initialize here
initialize();
})
}
This must work, remember to change the initialize function to some like this:
function initialize(){
var promise;
if(!currentLocation){
promise = loadWeather("Washington, DC");
}
else{
promise = loadWeather(currentLocation);
}
promise.then(function(html){
// some logic with succesful call
}, function(error) {
// some logic with error call
})
}
I'm trying to get the following findTimelineEntries function inside an Angular controller executing after saveInterview finishes:
$scope.saveInterview = function() {
$scope.interviewForm.$save({employeeId: $scope.employeeId}, function() {
$scope.findTimelineEntries();
});
};
The save action adds or edits data that also is part of the timeline entries and therefore I want the updated timeline entries to be shown.
First I tried changing it to this:
$scope.saveInterview = function() {
var functionReturned = $scope.interviewForm.$save({employeeId: $scope.employeeId});
if (functionReturned) {
$scope.findTimelineEntries();
}
};
Later to this:
$scope.saveInterview = function() {
$scope.interviewForm.$save({employeeId: $scope.employeeId});
};
$scope.saveInterview.done(function(result) {
$scope.findTimelineEntries();
});
And finaly I found some info about promises so I tried this:
$scope.saveInterview = function() {
$scope.interviewForm.$save({employeeId: $scope.employeeId});
};
var promise = $scope.saveInterview();
promise.done(function() {
$scope.findTimelineEntries();
});
But somehow the fact that it does work this way according to http://nurkiewicz.blogspot.nl/2013/03/promises-and-deferred-objects-in-jquery.html, doesn't mean that I can use the same method on those $scope.someFuntcion = function() functions :-S
Here is a sample using promises. First you'll need to include $q to your controller.
$scope.saveInterview = function() {
var d = $q.defer();
// do something that probably has a callback.
$scope.interviewForm.$save({employeeId: $scope.employeeId}).then(function(data) {
d.resolve(data); // assuming data is something you want to return. It could be true or anything you want.
});
return d.promise;
}