Return modal element, open it if necessary - angularjs

I'm refactoring my PageObject for my tests. Currently I'm checking the labels present in 2 different buttons in a modal.
/* home-spec.js */
it('Some test', function(){
expect(homePage.getButton1Label()).toEqual(expectations.btn1);
expect(homePage.getButton2Label(true)).toEqual(expectations.btn2);
});
Although this currently works I need to pass a variable indicating if the modal is open or not. That's the bit I'm trying to fix.
/* home-page.js */
var HomePage = function () {
function getModalContent(modalName, isModalOpen){
/* isModalOpen = element(by.css('.modal-content')).isPresent(); */
if(!isModalOpen){
var manageProductsView = getUiView('SOME_VIEW');
var btn = getButton(manageProductsView);
btn.click();
browser.waitForAngular();
}
return element(by.css('.modal-content'));
}
function getButtonLabel(buttonBinding, isModalOpen){
/* isModalOpen = element(by.css('.modal-content')).isPresent(); */
var modalcontent = getModalContent('MODAL_NAME', isModalOpen);
var modalFooter = modalcontent.element(by.css('.modal-footer'));
var btn = modalFooter.element(by.binding(buttonBinding));
return btn.getText();
}
return {
getButton1Label: function(isModalOpen){
return getButtonLabel('btn1', isModalOpen);
},
getButton2Label: function(isModalOpen){
return getButtonLabel('btn2', isModalOpen);
}
}
}
What I would like to do is remove that isModalOpen dependency but I don't seem to find the correct way to do it. The comments indicate what I've tried and seemed to be the way to go. Also tried to wrap it in the then block.
EDIT
Based on Vlad answer I edited my getButtonLabel function so it checks if the modal is open
function getButtonLabel(buttonBinding){
return element(by.css('.modal-content')).isPresent().then(function(isModalOpen){
var modalcontent = getModalContent('MODAL_NAME', isModalOpen);
var modalFooter = modalcontent.element(by.css('.modal-footer'));
var btn = modalFooter.element(by.binding(buttonBinding));
return btn.getText();
});
}
Was trying to avoid handling promises manually but I guess in some cases it's unavoidable

Your commented part is the way to go:
function getModalContent(modalName){
var modalContent = element(by.css('.modal-content'));
var isModalOpen = modalContent.isPresent();
return isModalOpen.then(function(open) {
if(!open){
var manageProductsView = getUiView('SOME_VIEW');
var btn = getButton(manageProductsView);
return btn.click()
.then(function(){
return modalContent;
});
}
return modalContent;
});
}
function getButtonLabel(buttonBinding){
var modalcontent = getModalContent('MODAL_NAME');
var btnText = modalcontent
.then(function(content) {
return content
.element(by.css('.modal-footer'))
.element(by.binding(buttonBinding))
.getText();
});
return btnText;
}
Beware that the modal remains open after the test, you might want to add something to close it if it's open to maintain consistent state throughout tests.
It's much better to know for sure if it's open before doing operations on it, so that you can open it yourself if it isn't and you need it to be, instead of doing conditionals like these - they are expensive because of promise chaining.
My general feeling is that the logic looks a bit too complicated, you might want to refactor some of the stuff in the future :p

Related

Create and destroy Angular watchers

I would like to turn off this watcher because it keeps hitting "kendoWidgetCreated" event over and over again, and causes an infinite loop where I hit the kendoGrid.refresh() .
How can I turn it off, then turn back on ?
scope.$on("kendoWidgetCreated", function (ev, widget) {
var kendoGrid = widget.element.parent().find('.k-grid').data("kendoGrid");
if (kendoGrid != undefined) {
kendoGrid.$angular_scope.compileTemplate();
kendoGrid.refresh();
}
});
I tried something like this, but couldn't get the watcher to trigger :
var kendoWidgetWatcher = scope.$watch("kendoWidgetCreated", refreshKendoWidgets);
var refreshKendoWidgets = function (ev, widget) {
// widget compile/refresh code here...
}
Advice is always appreciated...
regards,
Bob
***** UPDATE ****
My initial idea for creating an anonymous function was NOT working; however, Pankar's answer below worked for me.
Here's the updated, working version :
// setup new 'kendoWidgetWatcher' object for Kendo widget watcher, compile/refresh Kendo grids/charts
var kendoWidgetWatcher;
function registerWatcher() {
kendoWidgetWatcher = scope.$on("kendoWidgetCreated", refreshKendoWidgets);
}
function refreshKendoWidgets(ev, widget) {
var ht = widget.getSize().height;
var wt = widget.getSize().width;
var kendoGrid = widget.element.parent().find('.k-grid').data("kendoGrid");
if (kendoGrid != undefined) {
if (kendoWidgetWatcher) {
kendoWidgetWatcher(); // disable watch
}
kendoGrid.$angular_scope.compileTemplate(); // recompile the html tempate, then refresh kendo widget
kendoGrid.refresh();
registerWatcher(); // re-enable
}
}
You could easily turn off your your watcher by calling the watcher reference as function, and re-register it whenever you want it.
var kendoWidgetWatcher;
function refreshKendoWidgets(ev, widget) {
var kendoGrid = widget.element.parent().find('.k-grid').data("kendoGrid");
if (kendoGrid != undefined) {
kendoGrid.$angular_scope.compileTemplate();
kendoGrid.refresh();
// HOW TO DISABLE THE WATCH HERE ?
}
}
function registerWatcher (){
kendoWidgetWatcher = scope.$watch("kendoWidgetCreated", refreshKendoWidgets);
}
registerWatcher();
//you could call below code for re-registering the watcher
if(kendoWidgetWatcher)
kendoWidgetWatcher(); //to deregister it.
registerWatcher(); //re-register it.

Is it possible to prevent changing of router by clicking back button?

I'am building single page using BackboneJS and I need to prevent router executing on back button in a browser. To be exact I need to show confirmation custom popup with the text "Do you really want exit room? [yes|no]". So if user clicks yes then default actions should happens but if no then user should stay in the current screen.
I use Backbone.router with pushState: true. Does Backbonejs provide something like before router event to be possible prevent router handling or how could I archive it?
I'm not sure if this is still an issue, but this is how I would get around it. It may not be the best way, but could be a step in the right direction.
Backbone.History.prototype.loadUrl = function (fragment, options) {
var result = true;
if (fragment === void (0) && options === void (0) && this.confirmationDisplay !== void(0))
{
result = confirm('Are you sure you want to leave this room?');
}
var opts = options;
fragment = Backbone.history.fragment = Backbone.history.getFragment(fragment);
if (result) {
this.confirmationDisplay = true;
return _.any(Backbone.history.handlers, function (handler) {
if (handler.route.test(fragment)) {
//We just pass in the options
handler.callback(fragment, opts);
return true;
}
});
}
return this;
}
Essentially checking if we have a fragment and options, if not, we can assume the app just started, or the user clicked the back button.
Backbone router has an execute method which is called for every route change, we can return false to prevent the current transition. The code will probably look like below :
With an asynchronous popup (untested code, but should work)
Backbone.Route.extend({
execute: function(callback,args){
if(this.lastRoute === 'room'){
showPopup().done(function(){
callback & callback.apply(this,args);
}).fail(function(){
Backbone.history.navigate('room/486',{trigger:false});
});
}else{
callback && callback.apply(this,args);
}
},
showPopup: function(){
var html = "<<div><p>Do you really want to exit</p><button id='yes'>Yes</button><button id='no'>No</button></div>"
var promise = $.Deferred();
$('body').append(html);
$(document).on('click','button#yes',function(){
promise.resolve();
});
$(document).on('click','button#no',function(){
promise.reject();
});
return promise;
}
});
With synchronous confirm popup
Backbone.Route.extend({
execute: function(callback,args){
if(this.lastRoute === 'room'){
var conf = confirm("Do you really want to exit the room ?");
if(!conf){
//Change the route back to room
Backbone.history.navigate('room/486',{trigger:false});
return false;
}
};
callback && callback.apply(this,args);
}
});
References:
http://backbonejs.org/#Router-execute

Protractor: functions overloading

I try to extend ElementFinder library. I wondering how I can require different methods with the same names?
I want to make something like:
// spec.js
var ef1 = require('./ef_extend1.js');
var ef2 = require('./ef_extend2.js');
expect(column_resizer.ef1.getWidth()).toEqual(18);
expect(column_resizer.ef2.getWidth()).toEqual(18);
Now I have an error:
TypeError: Cannot read property 'getWidth' of undefined
My required libraries:
// ef_extend1.js
var ElementFinder = $('').constructor;
ElementFinder.prototype.getWidth = function() {
return this.getSize().then(function(size) {
return size.width + 1;
});
};
And the second one:
// ef_extend2.js
var ElementFinder = $('').constructor;
ElementFinder.prototype.getWidth = function() {
return this.getSize().then(function(size) {
return size.width;
});
};
I guess you've used a solution from Protractor issue #1102, but now it can be accomplished a bit easier after PR#1633, because ElementFinder is now exposed in protractor global variable:
protractor.ElementFinder.prototype.getWidth = function () {
return this.getSize().then(function (size) {
return size.width;
});
};
expect($('body').getWidth()).toBe(100);
Update:
As I said in the comment, ElementFinder can only be extended again and again. If you already had a method getWidth, and you extend ElementFinder with one more getWidth implementation, then the first one will be overriden, there should not be any conflict. But you'll have to keep them in strict order depending on when do you want to use appropriate set of methods:
require('./ef_extend1.js');
expect(column_resizer.getWidth()).toEqual(18);
require('./ef_extend2.js');
expect(column_resizer.getWidth()).toEqual(18);
Actually I've came with some alternative approach, but I do not think it will be nice to use, but anyway. Here is a sample module with extension methods:
// ef_extend1.js
// shortcut
var EF = protractor.ElementFinder;
// holds methods you want to add to ElementFinder prototype
var extend = {
getWidth: function () {
return this.getSize().then(function (size) {
return size.width;
});
}
};
// will hold original ElementFinder methods, if they'll get overriden
// to be able to restore them back
var original = {};
// inject desired methods to prototype and also save original methods
function register() {
Object.keys(extend).forEach(function (name) {
original[name] = EF.prototype[name]; // save original method
EF.prototype[name] = extend[name]; // override
});
}
// remove injected methods and return back original ones
// to keep ElementFinder prototype clean after each execution
function unregister() {
Object.keys(original).forEach(function (name) {
if (typeof original[name] === 'undefined') {
// if there was not such a method in original object
// then get rid of meaningless property
delete EF.prototype[name];
} else {
// restore back original method
EF.prototype[name] = original[name];
}
});
original = {};
}
// pass a function, which will be executed with extended ElementFinder
function execute(callback) {
register();
callback();
unregister();
}
module.exports = execute;
And you will use them like that, being able to run protractor commands in "isolated" environments, where each of them has it's own set of methods for ElementFinder:
var ef1 = require('./ef_extend1.js');
var ef2 = require('./ef_extend2.js');
ef1(function () {
expect(column_resizer.getWidth()).toEqual(18);
});
ef2(function () {
expect(column_resizer.getWidth()).toEqual(18);
});
I'm not quire sure about it, maybe I am over-engineering here and there are solutions much easier than that.

wait for backbone template to be appended to the dom

I am trying to bind events to elements that are placed by appending a backbone template:
appendEditTemplateAndSetEvents: function() {
var associatedCollection = App.Helpers.findAssociatedCollection(this.allCollections, this.associatedCollectionId);
var template = this.setEditTemplateForElement(associatedCollection.type);
var modalBody = this.$el.find('.modal-body');
modalBody.empty();
var firstModel = associatedCollection.at(0);
if(template.mainTemplate !== null) {
modalBody.append($('#edit-form-element-frame').html());
//each mode in collection
associatedCollection.each(function(model){
if(model.get('positionInContainer') === 1) {
firstModel = model;
}
console.log(model.attributes);
modalBody.find('.elements-in-editmodal-wrapper').append(template.mainTemplate(model.toJSON()));
});
}
if( template.templateValidation.length !== 0 ) {
modalBody.append('<hr><h3>Validateregels</h3>');
_.each(template.templateValidation, function(val, index) {
modalBody.append(val(firstModel.toJSON()));
});
}
//set listeners and handlers that apply when a edit modal is open
this.validationEventsForEditModal(firstModel);
this.editErrorMessagesInModal(firstModel);
},
Now the problem is that when the last two functions are called the html of the templates isn't appended yet so the the events are binded to an object with a length of 0.
Does anyone have a decent solution for this async problem? I tried $.Defferred but that did not work, but maybe someone get's it working.
I solved this by using this.$el.find(...) in the functions:
this.validationEventsForEditModal(firstModel);
this.editErrorMessagesInModal(firstModel);
I don't know if it's still an async problem, but this solves it.

Drupal.attachBehaviours with jQuery infinitescroll and jQuery masonry

I am a little desperate here. I have been reading everything I was able to find on Drupal.behaviours but obviously its still not enough. I try running a masonry grid with the infinitescroll plugin to attach the new images to the masonry. This works fine so far. The next thing I wanted to implement to my website is a hover effect (which shows information on the images) and later fancybox to show the images in a huger size.
(function ($) {
Drupal.behaviors.views_fluidgrid = {
attach: function (context) {
$('.views-fluidgrid-wrapper:not(.views-fluidgrid-processed)', context).addClass('views-fluidgrid-processed').each(function () {
// hide items while loading
var $this = $(this).css({opacity: 0}),
id = $(this).attr('id'),
settings = Drupal.settings.viewsFluidGrid[id];
$this.imagesLoaded(function() {
// show items after .imagesLoaded()
$this.animate({opacity: 1});
$this.masonry({
//the masonry settings
});
});
//implement the function of jquery.infinitescroll.min.js
$this.infinitescroll({
//the infinitescroll settings
},
//show new items and attach behaviours in callback
function(newElems) {
var newItems = $(newElems).css({opacity: 0});
$(newItems).imagesLoaded(function() {
$(newItems).animate({opacity: 1});
$this.masonry('appended', newItems);
Drupal.attachBehaviours(newItems);
});
});
});
}
};
})(jQuery);
Now I read that I need to Reattach the Drupal.behaviours if I want the hover event to also take place on the newly added content.
(function ($) {
Drupal.behaviors.imgOverlay = {
attach: function (context) {
var timeout;
$('.img_gallery').hover(function() {
$this = $(this);
timeout = setTimeout(change_opacity, 500);
}, reset_opacity);
function change_opacity() {
//set opacity to show the desired elements
}
function reset_opacity() {
clearTimeout(timeout);
//reset opacity to 0 on desired elements
}
}
};
})(jQuery)
Where do I now write the Drupal.attachBehaviours() to make it work actually? Or is there some other error I just dont see atm? I hope I wrote the question so that its understandable and maybe it also helps somebody else, since I experienced that there is no real "official" running Version of this combination in drupal 7.
Ok, the solution is actually pretty simple. When writing it correctly than it also runs. its of course not Drupal.attachBehaviours() but Drupal.attachBehaviors() . So this combination now works and I am finally relieved :).

Resources