Tuesday, July 14, 2015

Yield out of Promise hell



In Protractor, every action on an UI element is a promise. A promise is something that happens in the future. It is easy to mix present with the future, for example:
it('test_promise', function () {
    browser.get(
'http://localhost:8080/examples/test.html')
        .
then(function() {
           
var btnEle=element(by.id("b1"));
           
for(var i=0; i<5; i++){
              
console.log("clicking at "+i+" times");
              
btnEle.click().then(function(){
                  
console.log("clicked at "+i+" times");
               })
           }
        });
})

The output from the above code is:
clicking at 0 times
clicking at 1 times
clicking at 2 times
clicking at 3 times
clicking at 4 times
clicked at 5 times
clicked at 5 times
clicked at 5 times
clicked at 5 times
clicked at 5 times
What happens is the click() action is put into the promise queue and scheduled to happen in the future, while the for loop finishes in the present. If we want click() to record the clicking times correctly, we have to use another variable to record the times: 



it('test_promise2', function () {
    browser.get('http://localhost:8080/examples/test.html')
        .then(function() {
            var btnEle=element(by.id("b1"));
            var j=0;
            for(var i=0; i<5; i++){
                btnEle.click().then(function(){
                    j++;
                    console.log("clicked at "+j+" times");
                })
            }

        });
})
 
This trick can be used to get values from an array of promises:
it('test_getAllElements', function () {

    browser.get('http://localhost:8080/examples/test.html')
        .then(function() {
            var messages=[];
            var j=0;
            var defer = protractor.promise.defer();
            element.all(by.xpath("//body")).then(function(eles) {
                for (var i = 0; i < eles.length; i++) {
                    util.getNonInputText(eles[i]).then(function (text) {
                        if (util.isDefined(text) && text.length > 0) {
                            console.log("message is:" + text);
                            messages.push(text);
                        }



                        j++;
                        if (j === eles.length) {
                            console.log("fulfilled");
                            defer.fulfill(messages);
                        }
                    });
                }

            });


            defer.promise.then(function(result){
                console.log(result);
            });
        });
})





Even something as innocent as getting the ID of an element is a promise, to get the ID, you have to get it in then():




 Util.prototype.getId = function(element){
    return element.getAttribute("id").then(function(id){
       return id;
    });
};

In my tests, on my occasions, I have to calculate IDs based on known elements, so I have to write the logic like this:

var ele=element(by.name("abc"));
util.getId(ele).then(function(id){
    var anotherEle=element(by.id(id+"_btn"));
    anotherEle.click().then(function(){
        ...
    })
})

This is certainly less intuitive than getting the id directly:

var ele=element(by.name("abc"));
var id=util.getId(ele);
var anotherEle=element(by.id(id+"_btn"));


I wanted to find a way to make writing async code as intuitive as writing sync code, this led me to Generator.Here is some code that shows the basics of Generator:
function* f() {
   
console.log('before a');
   
yield 'a';
   
console.log('before b');
   
yield 'b';
   
console.log('before c');
   
yield 'c';
   
console.log('before d');
   
return 'd';
}

var g = f();
console.log("1");
console.log(g.next());
console.log("2");
console.log(g.next());
console.log("3");
console.log(g.next());
console.log("4");
console.log(g.next());




The output is:


1
before a
{ value: 'a', done: false }
2
before b
{ value: 'b', done: false }
3
before c
{ value: 'c', done: false }
4
before d
{ value: 'd', done: true }



function* f(), * makes function f a generator function. The code inside the generator function won’t get executed until next() is called; and execution inside the generator function will stop at yield until next() is called again. 
This feature is useful in making async code into sync. Say, one line of code creates a promise, the next line of code won’t get executed until the promise is executed. Library co (https://github.com/tj/co ) does exactly that. 


With co,   I can write code in a more intuitive way. For example, I can now get the id of an element:
co(function*() {
   
var ele = element(by.name("abc"));
   
var id=yield util.getId(ele);
   
var anotherEle=element(by.id(id+"_btn"));
});

The resulting code is much more structured and readable:

 

And by the way, in Webstorm, you need to configure javascript language in order for Webstorm to support yield syntax:




No comments:

Post a Comment