Executing an ElementArrayFinder - javascript

I am learning protractor and it has thus far been a wild journey because I am also pretty new to Javascript. I learned so far that protractor queues all promises and they can be executed using then().
However, I am now trying to use a filter() on an ElementArrayFinder but it doesn't seem to execute. Only when I preprend it with the return-keyword, the filter get's executed, but then I leave my function and I don't want that.
Can someone help me in understanding this please?
Below my code:
it('Select service', function() {
servicePage.services.filter(function(elem, index) {
return elem.getAttribute('class').then(function(attribute) {
console.log('*****' + attribute);
return attribute === 'service passive';
});
});
servicePage.services.get(0).element(by.tagName('input')).click();
});
When running above, the console log is not performed so I guess the filter function is not being executed. When I do it like below, the filter is executed but then the click() is not performed.
it('Select service', function() {
return servicePage.services.filter(function(elem, index) {
return elem.getAttribute('class').then(function(attribute) {
console.log('*****' + attribute);
return attribute === 'service passive';
});
});
servicePage.services.get(0).element(by.tagName('input')).click();
});
Example3:
it('Select service', function() {
servicePage.services.filter(function(elem, index) {
return elem.getAttribute('class').then(function(attribute) {
console.log('*****' + attribute);
return attribute === 'service passive';
});
}).first().element(by.tagName('input')).click();
});
Thanks in advance!
Regards

You should catch the element that filter function returns and then perform action on it. filter() function returns elements that match the criteria you specify in the function. In your case its returning an element that has a class attribute service passive. If there are more than one elements with same attribute, then you probably have to chain get() function to perform operation on the required element. Here's how -
servicePage.services.filter(function(elem, index) {
return elem.getAttribute('class').then(function(attribute) {
console.log('*****' + attribute);
return attribute === 'service passive';
});
}).element(by.tagName('input')).click(); //if only one element is returning, click on it
OR replace the last line with below line when there are more elements that match the criteria -
}).get(1).element(by.tagName('input')).click(); //click the second element in the array of elements returned
Hope it helps.

Related

How to avoid deeply nested promises in protractor

I have a 'select' element in a UI component from which I need to retrieve the selected option (if any). As a beginner in both JavaScript and protractor, I am having trouble figuring out how to accomplish this without a bunch of nested promises:
I have two locators -- one for the selector's current selection and one for all the options:
selector = element(by.model("something.someId"));
this.selectorOptions = element.all(by.repeater("repeat in someOptions | orderBy:'name'"));
getSelectedOption = function () {
return this.selector.getText().then( function (selectionText) {
return this.selectorOptions.filter(function (option) {
option.getText().then(function (optionText) {
if(optionText === selectionText) {
option.getAttribute("value").then(function (value) {
// Some logic here which uses the value to return an pojo representing the selection
})
}
})
})
})
};
The above is just awful and I am sure this can be done better. I have looked at a lot of examples, but I haven't found one that involves dealing with nested promises which need to take parameters and then do something conditional based on the value, so I am having difficultly applying them to my situation, mostly because I don't really feel comfortable with asynchronous programming yet. How can I take the mess above and refactor it into something that isn't a nested callback hell?
Maybe playing a little bit with promises, protractor, arguments and bind you could get it quite cleaner.
Then you are using the protractor filter method which needs a boolean to be returned, in order to filter your values. But, from the way you used it, maybe
you were looking for each():
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.each
I didn't have any chance to test the following code, so it may most probably not work :D
selector = element(by.model("something.someId"));
this.selectorOptions = element.all(by.repeater("repeat in someOptions | orderBy:'name'"));
getSelectedOption = function () {
return this.selector.getText().then(firstText.bind(this));
};
function firstText(text) {
return this.selectorOptions.filter(filterSelector.bind(this, text));
}
function filterSelector(text, option) {
return option.getText().then(optionText.bind(this, text, option));
}
function optionText(text, option, optionText) {
if(optionText === text) {
return option.getAttribute("value").then(someLogic);
}
}
function someLogic(value) {
console.log(value);
// value should be your value
// Some logic here which uses the value to return an pojo representing the selection
// return true or false, filter is still waiting for a boolean...
}
Another version just using arguments without function parameters. Specially follow the arguments which got printed, to see if the order is correct:
selector = element(by.model("something.someId"));
this.selectorOptions = element.all(by.repeater("repeat in someOptions | orderBy:'name'"));
getSelectedOption = function () {
return this.selector.getText().then(firstText.bind(this));
};
function firstText() {
console.log(arguments);
// arguments[0] should be your selectionText
return this.selectorOptions.filter(filterSelector.bind(this, arguments[0]));
}
function filterSelector() {
console.log(arguments);
// arguments[0] should be your previous selectionText
// arguments[1] should be your option
return arguments[1].getText().then(optionText.bind(this, arguments[0], arguments[1]));
}
function optionText() {
console.log(arguments);
// arguments[0] should be your optionText
// arguments[1] should be your selectionText
// arguments[2] should be your option
if(arguments[0] === arguments[1]) {
return arguments[2].getAttribute("value").then(someLogic);
}
}
function someLogic(value) {
console.log(value);
// value should be your value
// Some logic here which uses the value to return an pojo representing the selection
// return true or false, filter is still waiting for a boolean...
}

Reuse of functions

I think I know the theory behind the solution but I am having trouble implementing it. Consider following piece of code:
this.selectFirstPassiveService = function () {
getFirstPassiveService().element(by.tagName('input')).click();
}
this.clickAddTask = function () {
getFirstActiveService().element(by.tagName('a')).click();
}
this.selectTask = function () {
getFirstActiveService()
.element(by.tagName('option'))
.$('[value="0"]')
.click();
}
this.saveTask = function () {
getFirstActiveService().element(by.name('taskForm')).submit();
}
getFirstActiveService = function () {
return services.filter(function (elem) {
return elem.getAttribute('class').then(function (attribute) {
return attribute === 'service active ';
});
}).first();
}
getFirstPassiveService = function () {
return services.filter(function (elem) {
return elem.getAttribute('class').then(function (attribute) {
return attribute === 'service passive ';
});
}).first();
}
};
To minimalize code duplication, I created two functions:
* getFirstActiveService()
* getFirstPassiveService()
My spec goes as follows:
it('Select service', function () {
servicePage.selectFirstPassiveService();
servicePage.clickAddTask();
servicenPage.selectTask()();
});
Both clickAddTask() and selectTask() use the function called getFirstActiveService(). Everything runs fine in clickAddTask() but when I use the function in selectTask(), some elements (which are present) cannot be found by protractor.
Here goes my theory, every command in getFirstActiveService() is queued in the control flow when the function is called in clickAddTask() and is then executed. When reusing the function in selectTask() the commands aren't queued, the instance created in clickAddTask() is used and therefore, some elements cannot be found since the DOM has changed since then.
Now first question: Is my theory correct?
Second question: How can I fix this?
Thanks in advance!
Cheers
I refactored it a bit and it works now.
The problem was in the test itself, not in the reuse of functions.

Processing list of items serially in order and returning ok when all is done (async -> sync)

I'm itching head with concept of promises and async procedures. I have ordered a list and want to call a function with every item, wait until first procedure with the first item is done, proceed to second, third and so on. And only after every item is processed I want continue the main process.
Below is the code that made it well with the main process. So returning Q.all(promises) resulted that first all promises were processed and then main process continued. But problem was, that items (navigation keys) were processed async while I need them in sync:
function processPages(that) {
var navs = [];
Object.keys(that.navigation).map(function(key) {
navs.push({key: key, order: parseInt(that.navigation[key].index)});
});
var promises = navs.sort(function(a, b) {
return a.order - b.order;
})
.map(function(item) {
return that.parsePage(item.key).then(function(page) {
return page.sections.filter(function(section) {
return section.type == 'normal';
})
.map(function(section) {
collectStore(section, page, that);
});
});
});
return Q.all(promises);
}
Below is the code when I modified that items are processed in sync and right order, but now main process will be out of sync:
function processPages(that) {
var navs = [];
Object.keys(that.navigation).map(function(key) {
navs.push({key: key, order: parseInt(that.navigation[key].index)});
});
var promises = navs.sort(function(a, b) {
return a.order - b.order;
})
.reduce(function(previous, item) {
return previous.then(function () {
return that.parsePage(item.key).then(function(page) {
return page.sections.filter(function(section) {
return section.type == 'normal';
})
.map(function(section) {
collectStore(section, page, that);
});
});
});
}, Q());
return Q.all(promises);
}
Does anyone know what is happening here and how to use promises right way in this case?
Additional information
processPages is called from init hook. If promise (Q.all) is not used, then page hook may fire before init hook is totally processed, which I cannot allow either. This is what I refer with the "main process".
module.exports =
{
hooks: {
"init": function() {
var options = this.options.pluginsConfig['regexplace'] || {};
options.substitutes = options.substitutes || {};
// collects text replacement queries from plugin configuration
options.substitutes.forEach(function (option) {
patterns.push({re: new RegExp(option.pattern, option.flags || ''),
sub: option.substitute,
decode: option.decode || false,
store: option.store || null,
unreset: option.unreset || false});
});
this.config.book.options.variables = this.config.book.options.variables || {};
processPages(this);
},
"page": function(page) {
var that = this;
// process all normal sections in page
page.sections.filter(function(section) {
return section.type == 'normal';
})
.map(function(section) {
collectStore(section, page, that, true);
});
return page;
}
}
};
Code is part of the GitBook plugin code.
Take a look at the runnable examples (from Chrome's dev console) from the Understanding JavaScript Promise with Examples, especially the "chaining" example.
Based on your description of "...I have ordered a list and want to call a function with every item, wait until first procedure with the first item is done, proceed to second, third and so on. And only after every item is processed I want continue the main process.",
From algorithm point of view, you should be "chaining" multiple promises together:
create a Promise for every item. When an item is done, call resolve() so that then() will execute (next item in chain).
put the "main process" as the last item in the chain.
Recommend you test/learn promises' execution flow with simple example before applying it in your problem - makes it easier to understand.
Hope this helps! :-)
Solution was as simple as changing:
processPages(this);
to
return processPages(this);
on init hook.

Why assigning the same value triggers subscribe handler?

I have a simple viewmodel with an observalbe array of items, and an observable holding the selected item. I subscribe to the changes of selected item, and I can see in my tests that the handler is fired even when I assign the same value again and again, so there should not be any change. The following code shows 3 alerts with all the same "changed to ..." text.
view.SelectedItem(view.Items()[0]);
view.SelectedItem.subscribe(function(newValue) {
alert("changed to " + ko.toJSON(newValue));
});
view.SelectedItem(view.Items()[0]);
view.SelectedItem(view.Items()[0]);
view.SelectedItem(view.Items()[0]);
Here is a demo fiddle.
Apparently, selecting an item, even if it's the same one as what's already selected, triggers the change event, calling the function specified when subscribing.
If you want to be notified of the value of an observable before it is about to be changed, you can subscribe to the beforeChange event. For example:
view.SelectedItem.subscribe(function(oldValue) {
alert("The previous value is " + oldValue);
}, null, "beforeChange");
Source
This could help you determine whether or not the value has changed.
You can create function to have access to old and new values for compare it:
ko.subscribable.fn.subscribeChanged = function(callback) {
var previousValue;
this.subscribe(function(oldValue) {
previousValue = oldValue;
}, undefined, 'beforeChange');
this.subscribe(function(latestValue) {
callback(latestValue, previousValue);
});
};
You could add this function to some file with you ko extensions. I once found it on stackoverflow but can't remeber link now. And then you could use it like this:
view.SelectedItem.subscribeChanged(function(newValue, oldValue) {
if (newValue.Name != oldValue.Name || newValue.Quantity != oldValue.Quantity) {
alert("changed to " + ko.toJSON(newValue));
}
});
Fiddle
I ended up creating my own method based on a thread on a forum:
// Accepts a function(oldValue, newValue) callback, and triggers it only when a real change happend
ko.subscribable.fn.onChanged = function (callback) {
if (!this.previousValueSubscription) {
this.previousValueSubscription = this.subscribe(function (_previousValue) {
this.previousValue = _previousValue;
}, this, 'beforeChange');
}
return this.subscribe(function (latestValue) {
if (this.previousValue === latestValue) return;
callback(this.previousValue, latestValue);
}, this);
};

jQuery each, find, and append

Currently I have a function that iterates through multiple divs, finds a div with the class name of ".courseArea", and return it to be appended by another
function getCourseAreaBySemester(sem_id) {
$(".semesterPanel").each(function() {
if($(this).attr('id') == sem_id) {
return $(this).find('.courseArea'));
}
});
}
Second function to process the return
var targetSemester = getCourseAreaBySemester(semesterData[i]['semester_id']);
console.log(targetSemester);
targetSemester.append(createCourse(semesterData['courses'][j]));
The console prints out "undefined", and therefore, I cannot do a .append().
But if I console.log right before the return, it returns [<ul class=​"courseArea">​</ul>​]
I know it works If I don't use the .each() function. However, I need to select by ID.
Does anyone know what is wrong here? Or of an alternative? Thanks
function getCourseAreaBySemester(sem_id) {
var result;
$(".semesterPanel").each(function() {
if($(this).attr('id') == sem_id) {
result = $(this).find('.courseArea'));
}
});
return result;
}
Returning the value of the inner function inside it obviously does not affect the return value of the getCourseAreaBySemester function!
You aren't returning anything from getCourseAreaBySemester. A return within each is closed and only returns within the each callback
You could simplify and just use the ID as selector since ID's in a page are unique:
function getCourseAreaBySemester(sem_id) {
return $('#'+sem_id).find('.courseArea');
}

Categories

Resources