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


103

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

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

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


findElement () не ждет - что вы имеете в виду?
Ростислав Матл

1
findElementожидает, пока элемент будет доступен, но иногда элемент доступен до того, как код javascript будет полностью инициализирован, поэтому это не вариант.
psadac

Я забыл об этом - я привык использовать WebDriverWait и ExpectedCondition, это намного более гибко.
Ростислав Матл

Ответы:


73

Если бы кто-нибудь действительно знал общий и всегда применимый ответ, он был бы реализован повсюду много веков назад и сделал бы нашу жизнь НАСТОЛЬКО проще.

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

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

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

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

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


31

Спасибо, Эшвин!

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

исходя из вашего намека, это отлично сработало для меня:

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

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


2
Для меня это тоже очень хорошо работает. В моем случае элемент был изменен Javascript сразу после завершения загрузки страницы, и Selenium не ожидал этого неявно.
Noxxys

2
Откуда появился Predicate? какой импорт ??
Amado Saladino

@AmadoSaladino "import com.google.common.base.Predicate;" из google lib guava-15.0 ссылка: mvnrepository.com/artifact/com.google.guava/guava/15.0 есть более новая версия 27.0, вы можете попробовать!
Эдуардо Фабрицио

26

Вам нужно дождаться завершения загрузки 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);
}

2
Не уверен, что это сработает в любых обстоятельствах, но для моего
варианта

1
У меня это тоже работает, но есть одна хитрость: допустим, ваша страница уже загружена. Если вы взаимодействуете с элементом [например, отправляете .click () или .send некоторые ключи к нему], а затем хотите проверить, когда ваша страница закончила обработку, вам нужно добавить задержку: например, щелчок (), сон (0,5 сек. ) подождите, пока (readyState = 'complete' & jQuery.active == 0). Если вы не добавите спящий режим, iQuery не будет активен во время тестирования! (Мне потребовалось несколько часов, чтобы узнать, так что я решил поделиться этим)
Фивос Виланакис

document.readyState == 'complete' && jQuery.active == 0отлично работает, но есть ли что-то подобное для React? Поскольку эти проверки не улавливают все еще загружаемые данные / компонент реакции?
Rain9333 06

10

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

Если да, вы можете дождаться появления переменной. Ты можешь использовать

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

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

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



7

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

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

1
Я хотел бы отметить здесь, что я зарегистрировал время, которое потребовалось на то, чтобы дважды опросить страницу среднего размера, затем сравнить эти строки в java, и это заняло чуть более 20 мс.
TheFreddyKilo

2
Сначала я обнаружил, что это грязное решение, но, похоже, оно отлично работает и не требует настройки переменных на стороне клиента. Одним из недостатков может быть страница, которая постоянно изменяется с помощью javascript (т.е. часы javascript), но у меня этого нет, поэтому она работает!
Питер

4

Приведенный ниже код отлично работает в моем случае - моя страница содержит сложные 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


2
Вы должны использовать селеновые ожидания вместо этих циклов
cocorossello

4

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

WebDriverWait wait = new WebDriverWait(driver, 50);

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

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

1
Selenium также делает это по умолчанию, это просто добавит дополнительное время выполнения кода в ваш скрипт (которого может хватить для загрузки дополнительных вещей, но опять же, может и не быть)
Ardesco

2

Я попросил своих разработчиков создать переменную 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.");
}

1

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

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

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

ПРИМЕЧАНИЕ. Здесь вы можете увидеть весь мой рабочий пример .


1

Для nodejsбиблиотеки Selenium я использовал следующий фрагмент. В моем случае, я искал двух объектов, которые добавляются к окну, которое в этом примере <SOME PROPERTY>, 10000является таймаут миллисекунд, <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);
});

0

Вы можете написать логику, чтобы справиться с этим. Я написал метод, который вернет, WebElementи этот метод будет вызываться три раза, или вы можете увеличить время и добавить нулевую проверку для 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;
        }
    }

0

Вот из моего собственного кода:
Window.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;

В качестве бонуса счетчик может быть сброшен на document.readyState или на вызовах 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); " +
                            " } "+ 

...

РЕДАКТИРОВАТЬ: я заметил, что 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");
        }
    };
}

0

Не знаю, как это сделать, но в моем случае конец загрузки страницы и рендеринг совпадают с FAVICON, отображаемым на вкладке Firefox.

Итак, если мы можем получить изображение значка в веб-браузере, веб-страница будет полностью загружена.

Но как это сделать ....



-1

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

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

Selenium также делает это по умолчанию, это просто добавит дополнительное время выполнения кода в ваш скрипт (которого может хватить для загрузки дополнительных вещей, но опять же, может и не быть)
Ardesco

-1

Мне нравится ваша идея опросить HTML, пока он не станет стабильным. Я могу добавить это к своему собственному решению. Следующий подход основан на C # и требует jQuery.

Я разработчик тестового проекта SuccessFactors (SaaS), в котором мы не имеем никакого влияния на разработчиков или характеристики модели DOM, лежащей в основе веб-страницы. Продукт SaaS потенциально может изменять свой основной дизайн DOM 4 раза в год, поэтому постоянно ведется охота за надежными и производительными способами тестирования с помощью Selenium (включая НЕ тестирование с помощью Selenium, где это возможно!)

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

  • Driver это экземпляр WebDriver, который взаимодействует с браузером
  • DefaultPageLoadTimeout значение тайм-аута в тиках (100 нс за тик)

public IWebDriver Driver { get; private set; }

// ...

const int GlobalPageLoadTimeOutSecs = 10;
static readonly TimeSpan DefaultPageLoadTimeout =
    new TimeSpan((long) (10_000_000 * GlobalPageLoadTimeOutSecs));
Driver = new FirefoxDriver();

В дальнейшем обратите внимание на порядок ожидания в методе PageReady(готовность документа Selenium, Ajax, анимация), который имеет смысл, если вы задумаетесь:

  1. загрузить страницу, содержащую код
  2. используйте код для загрузки данных откуда-нибудь через Ajax
  3. представить данные, возможно, с анимацией

Что-то вроде вашего подхода к сравнению DOM можно использовать между 1 и 2, чтобы добавить еще один уровень надежности.


public void PageReady()
{
    DocumentReady();
    AjaxReady();
    AnimationsReady();
}


private void DocumentReady()
{
    WaitForJavascript(script: "return document.readyState", result: "complete");
}

private void WaitForJavascript(string script, string result)
{
    new WebDriverWait(Driver, DefaultPageLoadTimeout).Until(
        d => ((IJavaScriptExecutor) d).ExecuteScript(script).Equals(result));
}

private void AjaxReady()
{
    WaitForJavascript(script: "return jQuery.active.toString()", result: "0");
}

private void AnimationsReady()
{
    WaitForJavascript(script: "return $(\"animated\").length.toString()", result: "0");
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.