Implementing waitFor functionality with phantomjs-

2020-02-28 02:57发布

问题:

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.

回答1:

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


回答2:

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
    });


回答3:

I recently created a fairly simple node module to port waitFor over to node: https://gist.github.com/joseym/1d01edbcc40a7698f55a#file-phantomjs-waitfor-js

var async = require('async');

module.exports = waitFor;

/**
 * waitFor port used with 
 * @see    {@link https://github.com/ariya/phantomjs/blob/master/examples/waitfor.js}
 * @see    {@link https://github.com/sgentle/phantomjs-node}
 * @callback testFx - Test function, will repeat until true or timeout limit is reached
 * @callback onReady - Fires if/when `testFx` passes.
 * @param {(number|boolean|string)} [timeOut=false] - If defined and falsey or string value of`forever` 
 *                                                    then `waitFor` will run until `testFx` passes without 
 *                                                    timing out, otherwise pass a number in miliseconds.
 */
function waitFor(testFx, onReady, timeOut) {

    var maxtimeOutMillis = typeof timeOut !== 'undefined' ? timeOut : 5000 // Default Max Timout is 5s if not defined
        , start = new Date().getTime()
        , isAsync = testFx.length > 0
        , passing = undefined
    ;

    async.until(
        function Test() { 
            return typeof passing !== 'undefined'; 
        },
        function Action(cb) {
            setTimeout(function(){

                if (!maxtimeOutMillis || maxtimeOutMillis == 'forever' || new Date().getTime() - start < maxtimeOutMillis) {

                    // If a callback is passed to `testFx` we'll handle that.
                    function useCallback(){
                        passing = arguments[0]
                        return cb();
                    };                    

                    passing = (function(){
                        return (typeof(testFx) === "string" ? eval(testFx) : testFx).apply(this, arguments);
                    })(isAsync ? useCallback : undefined);

                    if(!isAsync) cb();

                } else {
                    return cb(new Error('`waitFor` timeout'));
                }

            }, 250);
        },
        function Done(err) {
            return (function(){
                return (typeof(onReady) === "string" ? eval(onReady) : onReady).apply(this, arguments);                  
            })(err, passing);
        }
    );

}