Реализация waitFor функциональность с phantomjs- node

Я пробовал и тестировал - с успехом - пример phantomjs waitFor. Тем не менее, мне трудно реализовать его с помощью phantomjs-node, прежде всего потому, что page.evaluate оценивается в обратном вызове.

Проект PhantomJS

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

Однако при phantomjs- node функция оценки возвращает возвращенные данные в обратном вызове:

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

Используя phantomjs- node, как я могу запустить функцию на странице только после того, как элемент виден?

На всякий случай, ссылка выше мертва, вот реализация функции waitFor

/**
* 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 supposed to do once the condition is fulfilled
                clearInterval(interval); //< Stop this interval
            }
        }
    }, 250); //< repeat check every 250ms
};

Спасибо заранее.

Ответ 1

Сегодня в этой проблеме возникла мысль, что я поделюсь своим решением.

  // 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()
    }
  }

Первым шагом является создание новой функции wait. Он принимает те же параметры, что и исходная функция waitFor, но работает немного по-другому. Вместо использования интервала мы должны запустить функцию wait рекурсивно, после того, как был вызван обратный вызов из тестовой функции testFx. Также обратите внимание, что вам действительно не нужно передавать значение для start, поскольку оно автоматически устанавливается.

  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

В этом примере я устанавливаю обратный вызов для функции testFx как обратный вызов page.evaluate, который возвращает значение true/false, основанное на том, удалось ли найти какой-либо элемент на странице. Кроме того, вы можете создать обратный вызов для page.evaluate, а затем вызвать от него обратный вызов testFx, как показано ниже:

  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

Я написал альтернативу для phantomjs-node под названием phridge. Вместо того, чтобы превращать все вызовы функций и назначения в асинхронные операции, он просто выполняет всю функцию внутри PhantomJS.

Я думаю, что ваша проблема может быть выполнена следующим образом:

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

Недавно я создал довольно простой модуль node для переноса waitFor до 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);
        }
    );

}