Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing waitFor functionality with phantomjs-node

I have tried and tested - with success - the phantomjs example waitFor. However, I am having difficulty implementing it via the phantomjs-node module primarily because page.evaluate gets evaluated in a callback.

PhantomJS Implementation

page.open("http://twitter.com/#!/sencha", function () {
    waitFor(function() {

        // This here is easy to do as the evaluate method returns immediately
        return page.evaluate(function() {
            return $("#signin-dropdown").is(":visible");
        });

    }, function() {
       console.log("The sign-in dialog should be visible now.");
       phantom.exit();
    });
  }
});

However, with phantomjs-node the evaluate function gets returned data in a callback:

page.evaluate(
    function(){ /* return thing */ },
    function callback(thing) {  /* write code for thing */ }
)

Using phantomjs-node, how can I run a function on the page only after an element is visible?

Just in case the link above is dead, here is the implementation of the waitFor function

/**
* Wait until the test condition is true or a timeout occurs. Useful for waiting
* on a server response or for a ui change (fadeIn, etc.) to occur.
*
* @param testFx javascript condition that evaluates to a boolean,
* it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
* as a callback function.
* @param onReady what to do when testFx condition is fulfilled,
* it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
* as a callback function.
* @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used.
*/
function waitFor(testFx, onReady, timeOutMillis) {
var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3000, //< Default Max Timout is 3s
    start = new Date().getTime(),
    condition = false,
    interval = setInterval(function() {
        if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
            // If not time-out yet and condition not yet fulfilled
            condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //<    defensive code
        } else {
            if(!condition) {
                // If condition still not fulfilled (timeout but condition is 'false')
                console.log("'waitFor()' timeout");
                phantom.exit(1);
            } else {
                // Condition fulfilled (timeout and/or condition is 'true')
                console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");
                typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled
                clearInterval(interval); //< Stop this interval
            }
        }
    }, 250); //< repeat check every 250ms
};

Thanks in advance.

like image 825
Jason J. Nathan Avatar asked Mar 10 '14 04:03

Jason J. Nathan


2 Answers

Ran into this problem today, thought I'd share my solution.

  // custom helper function
  function wait(testFx, onReady, maxWait, start) {
    var start = start || new Date().getTime()
    if (new Date().getTime() - start < maxWait) {
      testFx(function(result) {
        if (result) {
          onReady()
        } else {
          setTimeout(function() {
            wait(testFx, onReady, maxWait, start)
          }, 250)
        }
      })
    } else {
      console.error('page timed out')
      ph.exit()
    }
  }

The first step is creating a new wait function. It takes the same parameters as the original waitFor function, but works a little differently. Instead of using an interval, we have to run the wait function recursively, after the callback from the test function testFx has been triggered. Also, note that you don't actually need to pass in a value for start, as it gets set automatically.

  wait(function (cb) {
    return page.evaluate(function () 
      // check if something is on the page (should return true/false)
      return something
    }, cb)
  }, function () { // onReady function
    // code
  }, 5000) // maxWait

In this example, I'm setting the callback for testFx function as the callback to page.evaluate, which returns a true/false value based on whether or not it was able to find some element on the page. Alternatively, you could create your callback for page.evaluate and then trigger the testFx callback from it, as shown below:

  wait(function (cb) {
    return page.evaluate(function () 
      // check if something is on the page (should return true/false)
      return something
    }, function(result) {
      var newResult = doSomethingCrazy(result)
      cb(newResult)
    }
  }, function () { // onReady function
    // code
  }, 5000) // maxWait
like image 142
ameensol Avatar answered Sep 23 '22 18:09

ameensol


I've written an alternative for phantomjs-node called phridge. Instead of turning all function calls and assignments into async operations it just executes the whole function inside PhantomJS.

I think your problem could be accomplished like this:

phridge.spawn()
    .then(function (phantom) {
        return phantom.openPage(url);
    })
    .then(function (page) {
        return page.run(selector, function (selector, resolve, reject) {
            // this function runs inside PhantomJS bound to the webpage instance
            var page = this;
            var intervalId = setInterval(function () {
                var hasBeenFound = page.evaluate(function (selector) {
                    return Boolean(document.querySelector(selector));
                }, selector);

                if (hasBeenFound === false &&
                    /* check if there is still some time left  */) {
                    // wait for next interval
                    return;
                }

                clearInterval(intervalId);

                if (hasBeenFound) {
                    resolve();
                } else {
                    reject(new Error("Wait for " + selector + " timeout"));
                }
            }, 100);
        });
    })
    .then(function () {
        // element has been found
    })
    .catch(function (err) {
        // element has not been found
    });
like image 32
Johannes Ewald Avatar answered Sep 25 '22 18:09

Johannes Ewald