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, тому це не варіант.
псадак

Я забув це - я звик використовувати WebDriverWait та ExpectedCondition, це набагато гнучкіше.
Ростислав Матл

Відповіді:


73

Якби хтось насправді знав загальну і завжди застосовну відповідь, вона була б реалізована скрізь століттями назад і зробила б наше життя набагато простішим.

Ви можете зробити багато речей, але кожна з них має проблеми:

  1. Як сказав Ешвін Прабху, якщо ви добре знаєте сценарій, ви можете спостерігати за його поведінкою і відслідковувати деякі його змінні на windowабо documentт.д. сторінок.

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

    • Потрібно багато часу, щоб фактично затвердити сторінку, і це може значно продовжити тест.
    • Ніколи не знаєш, що таке правильний інтервал. Сценарій може завантажувати щось велике, що займає більше 500 мс. На внутрішній сторінці нашої компанії є кілька сценаріїв, які займають кілька секунд в IE. Можливо, у комп’ютера тимчасово не вистачає ресурсів - скажімо, що антивірус змусить ваш CPU працювати повноцінно, то 500 мс може бути занадто коротким навіть для некомплексних сценаріїв.
    • Деякі сценарії ніколи не робляться. Вони називають себе з деякою затримкою ( setTimeout()) і працюють знову і знову і, можливо, можуть змінювати HTML щоразу, коли вони запускаються. Серйозно, це робить кожна сторінка "Веб 2.0". Навіть переповнення стека. Ви можете перезаписати найпоширеніші методи, що використовуються, і вважати сценарії, які використовують їх, як завершені, але ... ви не можете бути впевнені.
    • Що робити, якщо сценарій робить щось інше, ніж зміна HTML? Це може зробити тисячі речей, а не просто innerHTMLвесело.
  3. Є інструменти, які допоможуть вам у цьому. А саме слухачі прогресу разом з 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
Звідки походить предикат? який імпорт ??
Амадо Саладіно

@AmadoSaladino "імпортувати 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 () або. Надішліть до нього кілька клавіш], а потім ви хочете перевірити, коли ваша сторінка закінчена обробка, вам потрібно додати затримку: наприклад, натисніть (), спати (0,5 сек.) ), зачекайте, поки (readyState = 'завершено' & jQuery.active == 0). Якщо ви не додасте сну, iQuery не буде активним у тестовий час! (на це знадобилося кілька годин, тому я подумав поділитися нею)
Fivos Vilanakis

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

10

Чи визначає / ініціалізує бібліотека JS будь-яку відому змінну у вікні?

Якщо так, ви можете дочекатися появи змінної. Можна використовувати

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

перевірити на цей стан (щось на кшталт window.SomeClass && window.SomeClass.variable != null:) і повернути булеве значення true/ false.

Загорніть це в а WebDriverWaitта зачекайте, поки сценарій повернеться true.


7

У мене було таке ж питання. Це рішення працює для мене від 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


спасибі. Код, який знаходиться за посиланням, працює для мене,
ecamur

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


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 спробуйте знайтиOut webElement.

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

1
Селен також робить це за замовчуванням, це просто додасть додатковий час виконання коду до вашого сценарію (що може бути достатньо часу для завантаження додаткових речей, але потім знову може не бути)
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); " +
                            " } "+ 

...

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

0

Не знаю, як це зробити, але в моєму випадку кінець завантаження сторінки та рендерінг збігаються з FAVICON, що відображається на вкладці Firefox.

Тож якщо ми зможемо отримати зображення favicon у веб-браузері, веб-сторінка завантажена повністю.

Але як це виконати….



-1

Ось як я це роблю:

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

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

-1

Мені подобається ваша ідея опитування HTML, поки вона не стабільна. Я можу додати це до власного рішення. Наступний підхід є в C # і вимагає jQuery.

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

Ось що я використовую для "сторінки готової". Зараз він працює у всіх моїх власних тестах. Цей же підхід працював і для великого внутрішнього веб-додатку Java пару років тому, і він був надійним протягом року, коли я покинув проект.

  • Driver - це екземпляр WebDriver, який спілкується з браузером
  • DefaultPageLoadTimeout - значення тайм-аута в кліщах (100ns на галочку)

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.