Selenium WebDriver: дождитесь загрузки сложной страницы с JavaScript


у меня есть веб-приложение для тестирования с Selenium. Существует много JavaScript работает на странице загрузки.
Этот код JavaScript не так хорошо написан, но я ничего не могу изменить. Поэтому ждем, когда элемент появится в DOM с findElement() метод не является опцией.
Я хочу создать универсальную функцию в Java, чтобы дождаться загрузки страницы, возможным решением будет:

  • запустите скрипт JavaScript form WebDriver и сохраните результат document.body.innerHTML в a строковая переменная body.
  • сравнить body переменная к предыдущей версии body. если они одинаковы, то установите инкремент счетчика notChangedCount в противном случае установите для параметра notChangedCount к нулю.
  • подождите немного времени (например, 50 мс).
  • если страница не изменилась в течение некоторого времени (500 мс например) так notChangedCount >= 10 затем выйдите из цикла в противном случае цикл к первому шагу.

как вы думаете, это правильное решение?

14 82

14 ответов:

если бы кто-нибудь действительно знал общий и всегда применимый ответ, он был бы реализован везде давным-давно и сделал бы нашу жизнь намного проще.

есть много вещей вы можете сделать, но каждый из них имеет проблему:

  1. как сказал Ашвин Прабху, если вы хорошо знаете сценарий, вы можете наблюдать его поведение и отслеживать некоторые из его переменных на window или document etc. Это решение, однако, не для все и может быть использовано только вами и только на ограниченном наборе страниц.

  2. ваше решение, наблюдая за HTML-кодом и независимо от того, было ли оно изменено или не было изменено в течение некоторого времени, не плохо (кроме того, есть метод чтобы получить исходный и не отредактированный HTML напрямую WebDriver), но:

    • требуется много времени, чтобы фактически утверждать страницу и может значительно продлить тест.
    • вы никогда не знаете, что право интервал. Сценарий может загружайте что-то большое, что занимает более 500 мс. на внутренней странице нашей компании есть несколько сценариев, которые занимают несколько секунд в IE. Ваш компьютер может быть временно не хватает ресурсов-скажем, что антивирус сделает ваш процессор работать полностью, то 500 мс может быть слишком коротким даже для несложных сценариев.
    • некоторые скрипты не делал. Они называют себя с некоторой задержкой (setTimeout()) и работать снова и снова и может возможно, изменить HTML каждый раз, когда они работают. Серьезно, каждая страница "Web 2.0" делает это. Даже Переполнение Стека. Вы можете перезаписать наиболее распространенные используемые методы и считать сценарии, которые их используют, завершенными, но ... вы не можете быть уверены.
    • что делать, если скрипт делает что-то другое, чем изменение HTML? Он может делать тысячи вещей, а не только некоторые innerHTML удовольствие.
  3. есть инструменты, которые помогут вам в этом. А именно прогресс Слушатели вместе с nsIWebProgressListener и некоторые другие. Поддержка браузера для этого, однако, ужасна. Firefox начал пытаться поддерживать его с FF4 и далее (все еще развивается), IE имеет базовую поддержку в IE9.

и я думаю, что я мог бы придумать еще одно ошибочное решение в ближайшее время. Дело в том, что нет определенного ответа на вопрос, когда сказать "теперь страница завершена" из-за вечных сценариев, выполняющих свою работу. Выберите тот, который служит вам лучше всего, но остерегайтесь его недостатков.

Спасибо Ашвин !

в моем случае мне нужно ждать выполнения плагина jquery в каком-либо элементе.. в частности, "qtip"

основываясь на вашем намеке, он отлично работал для меня:

wait.until( new Predicate<WebDriver>() {
            public boolean apply(WebDriver driver) {
                return ((JavascriptExecutor)driver).executeScript("return document.readyState").equals("complete");
            }
        }
    );

Примечание: я использую Webdriver 2

вам нужно дождаться завершения загрузки Javascript и jQuery. Выполните Javascript, чтобы проверить, если jQuery.active и 0 и document.readyState - это complete, что означает, что загрузка JS и jQuery завершена.

public boolean waitForJStoLoad() {

    WebDriverWait wait = new WebDriverWait(driver, 30);

    // wait for jQuery to load
    ExpectedCondition<Boolean> jQueryLoad = new ExpectedCondition<Boolean>() {
      @Override
      public Boolean apply(WebDriver driver) {
        try {
          return ((Long)executeJavaScript("return jQuery.active") == 0);
        }
        catch (Exception e) {
          return true;
        }
      }
    };

    // wait for Javascript to load
    ExpectedCondition<Boolean> jsLoad = new ExpectedCondition<Boolean>() {
      @Override
      public Boolean apply(WebDriver driver) {
        return executeJavaScript("return document.readyState")
            .toString().equals("complete");
      }
    };

  return wait.until(jQueryLoad) && wait.until(jsLoad);
}

определяет ли библиотека JS / инициализирует какую-либо хорошо известную переменную в окне?

если это так, вы можете подождать появления переменной. Вы можете использовать

((JavascriptExecutor)driver).executeScript(String script, Object... args)

чтобы проверить это условие (что-то вроде: window.SomeClass && window.SomeClass.variable != null) и возвращает логическое true/false.

оберните это в WebDriverWait, и ждать, пока скрипт не вернется true.

ниже код прекрасно работает в моем случае - моя страница содержит сложные Java-скрипты

public void checkPageIsReady() {

  JavascriptExecutor js = (JavascriptExecutor)driver;


  //Initially bellow given if condition will check ready state of page.
  if (js.executeScript("return document.readyState").toString().equals("complete")){ 
   System.out.println("Page Is loaded.");
   return; 
  } 

  //This loop will rotate for 25 times to check If page Is ready after every 1 second.
  //You can replace your value with 25 If you wants to Increase or decrease wait time.
  for (int i=0; i<25; i++){ 
   try {
    Thread.sleep(1000);
    }catch (InterruptedException e) {} 
   //To check page ready state.
   if (js.executeScript("return document.readyState").toString().equals("complete")){ 
    break; 
   }   
  }
 }

источник Как Дождаться Загрузки/Готовности Страницы В Selenium WebDriver

У меня была такая же проблема. Это решение работает для меня от WebDriverDoku:

WebDriverWait wait = new WebDriverWait(driver, 10);
WebElement element = wait.until(ExpectedConditions.elementToBeClickable(By.id("someid")));

http://www.seleniumhq.org/docs/04_webdriver_advanced.jsp

Если все, что вам нужно сделать, это подождать, пока html на странице станет стабильным, прежде чем пытаться взаимодействовать с элементами, вы можете периодически опрашивать DOM и сравнивать результаты, если DOMs одинаковы в течение данного времени опроса, вы Золотой. Что-то вроде этого, где вы проходите в максимальное время ожидания и время между опросами страниц перед сравнением. Просто и эффективно.

public void waitForJavascript(int maxWaitMillis, int pollDelimiter) {
    double startTime = System.currentTimeMillis();
    while (System.currentTimeMillis() < startTime + maxWaitMillis) {
        String prevState = webDriver.getPageSource();
        Thread.sleep(pollDelimiter); // <-- would need to wrap in a try catch
        if (prevState.equals(webDriver.getPageSource())) {
            return;
        }
    }
}

на nodejs библиотека Селена, я использовал следующий фрагмент. В моем случае я искал два объекта, которые добавляются в окно, которые в этом примере являются <SOME PROPERTY>,10000 - это timeout миллисекунд, <NEXT STEP HERE> что происходит после того, как свойства находятся на окне.

driver.wait( driver => {
    return driver.executeScript( 'if(window.hasOwnProperty(<SOME PROPERTY>) && window.hasOwnProperty(<SOME PROPERTY>)) return true;' ); }, 10000).then( ()=>{
        <NEXT STEP HERE>
}).catch(err => { 
    console.log("looking for window properties", err);
});

Я попросил своих разработчиков создать переменную JavaScript "isProcessing", к которой я могу получить доступ (в объекте" ae"), который они устанавливают, когда все начинает работать и очищается, когда все делается. Затем я запускаю его в аккумуляторе, который проверяет его каждые 100 мс, пока он не получит пять подряд в общей сложности 500 мс без каких-либо изменений. Если проходит 30 секунд, я бросаю исключение, потому что что-то должно было произойти к тому времени. Это в C#.

public static void WaitForDocumentReady(this IWebDriver driver)
{
    Console.WriteLine("Waiting for five instances of document.readyState returning 'complete' at 100ms intervals.");
    IJavaScriptExecutor jse = (IJavaScriptExecutor)driver;
    int i = 0; // Count of (document.readyState === complete) && (ae.isProcessing === false)
    int j = 0; // Count of iterations in the while() loop.
    int k = 0; // Count of times i was reset to 0.
    bool readyState = false;
    while (i < 5)
    {
        System.Threading.Thread.Sleep(100);
        readyState = (bool)jse.ExecuteScript("return ((document.readyState === 'complete') && (ae.isProcessing === false))");
        if (readyState) { i++; }
        else
        {
            i = 0;
            k++;
        }
        j++;
        if (j > 300) { throw new TimeoutException("Timeout waiting for document.readyState to be complete."); }
    }
    j *= 100;
    Console.WriteLine("Waited " + j.ToString() + " milliseconds. There were " + k + " resets.");
}

чтобы сделать это правильно, вам нужно обрабатывать исключения.

вот как я делаю ожидание iFrame. Для этого требуется, чтобы ваш тестовый класс JUnit передал экземпляр RemoteWebDriver в объект page:

public class IFrame1 extends LoadableComponent<IFrame1> {

    private RemoteWebDriver driver;

    @FindBy(id = "iFrame1TextFieldTestInputControlID" )
    public WebElement iFrame1TextFieldInput;

    @FindBy(id = "iFrame1TextFieldTestProcessButtonID" )
    public WebElement copyButton;

    public IFrame1( RemoteWebDriver drv ) {
        super();
        this.driver = drv;
        this.driver.switchTo().defaultContent();
        waitTimer(1, 1000);
        this.driver.switchTo().frame("BodyFrame1");
        LOGGER.info("IFrame1 constructor...");
    }

    @Override
    protected void isLoaded() throws Error {        
        LOGGER.info("IFrame1.isLoaded()...");
        PageFactory.initElements( driver, this );
        try {
            assertTrue( "Page visible title is not yet available.", driver
     .findElementByCssSelector("body form#webDriverUnitiFrame1TestFormID h1")
                    .getText().equals("iFrame1 Test") );
        } catch ( NoSuchElementException e) {
            LOGGER.info("No such element." );
            assertTrue("No such element.", false);
        }
    }

    @Override
    protected void load() {
        LOGGER.info("IFrame1.load()...");
        Wait<WebDriver> wait = new FluentWait<WebDriver>( driver )
                .withTimeout(30, TimeUnit.SECONDS)
                .pollingEvery(5, TimeUnit.SECONDS)
                .ignoring( NoSuchElementException.class ) 
                .ignoring( StaleElementReferenceException.class ) ;
            wait.until( ExpectedConditions.presenceOfElementLocated( 
            By.cssSelector("body form#webDriverUnitiFrame1TestFormID h1") ) );
    }
....

Примечание:посмотреть весь мой рабочий пример здесь.

вы можете написать некоторую логику, чтобы справиться с этим. У меня есть написать метод, который будет возвращать WebElement и этот метод будет вызван три раза или можно увеличить время и добавить проверку на null для WebElement вот пример

public static void main(String[] args) {
        WebDriver driver = new FirefoxDriver();
        driver.get("https://www.crowdanalytix.com/#home");
        WebElement webElement = getWebElement(driver, "homekkkkkkkkkkkk");
        int i = 1;
        while (webElement == null && i < 4) {
            webElement = getWebElement(driver, "homessssssssssss");
            System.out.println("calling");
            i++;
        }
        System.out.println(webElement.getTagName());
        System.out.println("End");
        driver.close();
    }

    public static WebElement getWebElement(WebDriver driver, String id) {
        WebElement myDynamicElement = null;
        try {
            myDynamicElement = (new WebDriverWait(driver, 10))
                    .until(ExpectedConditions.presenceOfElementLocated(By
                            .id(id)));
            return myDynamicElement;
        } catch (TimeoutException ex) {
            return null;
        }
    }

вот из моего собственного кода:
Окно.setTimeout выполняется только тогда, когда браузер простаивает.
Таким образом, вызов функции рекурсивно (42 раза) займет 100 мс, если в браузере нет активности и многое другое, если браузер занят чем-то другим.

    ExpectedCondition<Boolean> javascriptDone = new ExpectedCondition<Boolean>() {
        public Boolean apply(WebDriver d) {
            try{//window.setTimeout executes only when browser is idle,
                //introduces needed wait time when javascript is running in browser
                return  ((Boolean) ((JavascriptExecutor) d).executeAsyncScript( 

                        " var callback =arguments[arguments.length - 1]; " +
                        " var count=42; " +
                        " setTimeout( collect, 0);" +
                        " function collect() { " +
                            " if(count-->0) { "+
                                " setTimeout( collect, 0); " +
                            " } "+
                            " else {callback(" +
                            "    true" +                            
                            " );}"+                             
                        " } "
                    ));
            }catch (Exception e) {
                return Boolean.FALSE;
            }
        }
    };
    WebDriverWait w = new WebDriverWait(driver,timeOut);  
    w.until(javascriptDone);
    w=null;

в качестве бонуса счетчик может быть сброшен на документ.в зависимости или на jQuery AJAX-вызовы или если какие-либо jQuery анимации работают (только если ваше приложение использует jQuery для AJAX-вызовы...)
...

" function collect() { " +
                            " if(!((typeof jQuery === 'undefined') || ((jQuery.active === 0) && ($(\":animated\").length === 0))) && (document.readyState === 'complete')){" +
                            "    count=42;" +
                            "    setTimeout( collect, 0); " +
                            " }" +
                            " else if(count-->0) { "+
                                " setTimeout( collect, 0); " +
                            " } "+ 

...

EDIT: я замечаю, что executeAsyncScript не работает хорошо, если загружается новая страница, и тест может перестать отвечать неопределенно, лучше использовать это вместо этого.

public static ExpectedCondition<Boolean> documentNotActive(final int counter){ 
    return new ExpectedCondition<Boolean>() {
        boolean resetCount=true;
        @Override
        public Boolean apply(WebDriver d) {

            if(resetCount){
                ((JavascriptExecutor) d).executeScript(
                        "   window.mssCount="+counter+";\r\n" + 
                        "   window.mssJSDelay=function mssJSDelay(){\r\n" + 
                        "       if((typeof jQuery != 'undefined') && (jQuery.active !== 0 || $(\":animated\").length !== 0))\r\n" + 
                        "           window.mssCount="+counter+";\r\n" + 
                        "       window.mssCount-->0 &&\r\n" + 
                        "       setTimeout(window.mssJSDelay,window.mssCount+1);\r\n" + 
                        "   }\r\n" + 
                        "   window.mssJSDelay();");
                resetCount=false;
            }

            boolean ready=false;
            try{
                ready=-1==((Long) ((JavascriptExecutor) d).executeScript(
                        "if(typeof window.mssJSDelay!=\"function\"){\r\n" + 
                        "   window.mssCount="+counter+";\r\n" + 
                        "   window.mssJSDelay=function mssJSDelay(){\r\n" + 
                        "       if((typeof jQuery != 'undefined') && (jQuery.active !== 0 || $(\":animated\").length !== 0))\r\n" + 
                        "           window.mssCount="+counter+";\r\n" + 
                        "       window.mssCount-->0 &&\r\n" + 
                        "       setTimeout(window.mssJSDelay,window.mssCount+1);\r\n" + 
                        "   }\r\n" + 
                        "   window.mssJSDelay();\r\n" + 
                        "}\r\n" + 
                        "return window.mssCount;"));
            }
            catch (NoSuchWindowException a){
                a.printStackTrace();
                return true;
            }
            catch (Exception e) {
                e.printStackTrace();
                return false;
            }
            return ready;
        }
        @Override
        public String toString() {
            return String.format("Timeout waiting for documentNotActive script");
        }
    };
}

два условия могут быть использованы, чтобы проверить, если страница загружается, прежде чем найти какой-либо элемент на странице:

WebDriverWait wait = new WebDriverWait(driver, 50);

использование ниже redayState будет ждать загрузки страницы

wait.until((ExpectedCondition<Boolean>) wd ->
   ((JavascriptExecutor) wd).executeScript("return document.readyState").equals("complete"));

ниже JQuery будет ждать, пока данные не будут загружены

  int count =0;
            if((Boolean) executor.executeScript("return window.jQuery != undefined")){
                while(!(Boolean) executor.executeScript("return jQuery.active == 0")){
                    Thread.sleep(4000);
                    if(count>4)
                        break;
                    count++;
                }
            }

после этих JavaScriptCode попробуйте найти webElement.

WebElement we = wait.until(ExpectedConditions.presenceOfElementLocated(by));

вот как я делаю это:

new WebDriverWait(driver, 20).until(
       ExpectedConditions.jsReturnsValue(
                   "return document.readyState === 'complete' ? true : false"));