Selenium WebDriver: Poczekaj na załadowanie złożonej strony z JavaScriptem

103

Mam aplikację internetową do przetestowania z Selenium. Podczas ładowania strony działa dużo JavaScript.
Ten kod JavaScript nie jest dobrze napisany, ale nie mogę nic zmienić. Czekam więc na pojawienie się elementu w DOM zfindElement() metodą nie jest opcją.
Chcę utworzyć ogólną funkcję w Javie, aby czekać na załadowanie strony, możliwym rozwiązaniem byłoby:

  • uruchom skrypt JavaScript z WebDriver i zapisz wynik document.body.innerHTMLw zmiennej łańcuchowej body.
  • porównaj bodyzmienną z poprzednią wersją body. jeśli są takie same, ustaw przyrost licznika w przeciwnym notChangedCountrazie ustawiony notChangedCountna zero.
  • poczekaj chwilę (na przykład 50 ms).
  • jeśli strona nie zmieniła się od jakiegoś czasu (na przykład 500 ms), tak notChangedCount >= 10 wyjdź z pętli, w przeciwnym razie przejdź do pierwszego kroku.

Czy uważasz, że to prawidłowe rozwiązanie?

psadac
źródło
findElement () nie czeka - co masz na myśli?
Rostislav Matl
1
findElementczeka, aż element będzie dostępny, ale czasami element jest dostępny, zanim kod javascript zostanie całkowicie zainicjowany, dlatego nie jest to opcja.
psadac
Zapomniałem - jestem przyzwyczajony do korzystania z WebDriverWait i ExpectedCondition, które są o wiele bardziej elastyczne.
Rostislav Matl

Odpowiedzi:

73

Gdyby ktokolwiek rzeczywiście znał ogólną i zawsze stosowną odpowiedź, byłaby ona wdrożona wszędzie wieki temu i znacznie ułatwiłaby nam życie.

Jest wiele rzeczy, które możesz zrobić, ale każda z nich ma problem:

  1. Jak powiedział Ashwin Prabhu, jeśli dobrze znasz skrypt, możesz obserwować jego zachowanie i śledzić niektóre z jego zmiennych na windowlub documentitp. To rozwiązanie nie jest jednak dla wszystkich i może być używane tylko przez Ciebie i tylko na ograniczonym zestawie stron.

  2. Twoje rozwiązanie poprzez obserwację kodu HTML i to, czy był, czy nie był zmieniany przez jakiś czas, nie jest złe (istnieje również metoda bezpośredniego uzyskania oryginalnego i nieedytowanego HTML WebDriver), ale:

    • Potwierdzenie strony zajmuje dużo czasu i może znacznie wydłużyć test.
    • Nigdy nie wiesz, jaki jest właściwy interwał. Skrypt może pobierać coś dużego, co trwa dłużej niż 500 ms. Na wewnętrznej stronie naszej firmy znajduje się kilka skryptów, które w IE zajmują kilka sekund. Twój komputer może chwilowo nie mieć zasobów - powiedz, że antywirus sprawi, że twój procesor będzie w pełni działał, wtedy 500 ms może być za krótkie nawet dla nieskomplikowanych skryptów.
    • Niektóre skrypty nigdy nie są gotowe. Wywołują siebie z pewnym opóźnieniem ( setTimeout()) i pracują wielokrotnie i mogą zmieniać kod HTML za każdym razem, gdy są uruchamiane. Poważnie, każda strona „Web 2.0” to robi. Nawet przepełnienie stosu. Możesz nadpisać najczęściej używane metody i uznać skrypty, które ich używają, za ukończone, ale ... nie możesz być pewien.
    • A jeśli skrypt robi coś innego niż zmiana kodu HTML? Może zrobić tysiące rzeczy, a nie tylko innerHTMLzabawić.
  3. Istnieją narzędzia, które Ci w tym pomogą. Mianowicie Progress Listeners wraz z nsIWebProgressListener i kilkoma innymi. Obsługa tego przez przeglądarkę jest jednak straszna. Firefox zaczął próbować go obsługiwać od FF4 (wciąż się rozwija), IE ma podstawowe wsparcie w IE9.

I myślę, że wkrótce mógłbym wymyślić inne wadliwe rozwiązanie. Fakt jest taki, że nie ma jednoznacznej odpowiedzi, kiedy powiedzieć „teraz strona jest kompletna”, ponieważ odwieczne skrypty wykonują swoją pracę. Wybierz ten, który najlepiej Ci służy, ale uważaj na jego wady.

Petr Janeček
źródło
31

Dzięki Ashwin!

W moim przypadku powinienem poczekać na wykonanie wtyczki jquery w jakimś elemencie .. konkretnie "qtip"

w oparciu o twoją wskazówkę, zadziałało idealnie dla mnie:

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

Uwaga: używam Webdriver 2

Eduardo Fabricio
źródło
2
U mnie też działa bardzo dobrze. W moim przypadku element został zmodyfikowany przez Javascript zaraz po zakończeniu ładowania strony, a Selenium nie czekał na to niejawnie.
Noxxys
2
Skąd się bierze Predicate? który import ??
Amado Saladino
@AmadoSaladino "import com.google.common.base.Predicate;" z google lib guava-15.0 link: mvnrepository.com/artifact/com.google.guava/guava/15.0 jest nowsza wersja 27.0 możesz spróbować!
Eduardo Fabricio,
26

Musisz poczekać na zakończenie ładowania JavaScript i jQuery. Wykonaj JavaScript, aby sprawdzić, czy jQuery.activejest 0i document.readyStatejest complete, co oznacza, że ​​ładowanie JS i jQuery zostało zakończone.

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);
}
LINGS
źródło
2
Nie jestem pewien, czy to zadziała w każdych okolicznościach, ale działa to nie lada gratka w moim przypadku użycia
DaveH
1
U mnie też to działa, ale jest trudna część: powiedzmy, że Twoja strona jest już załadowana. Jeśli wchodzisz w interakcję z elementem [np. Wyślij .click () lub. Wyślij do niego kilka kluczy] i chcesz sprawdzić, kiedy strona się zakończy, musisz dodać opóźnienie: np. Click (), uśpienie (0,5 sek. ), poczekaj, aż (readyState = 'complete' & jQuery.active == 0). Jeśli nie dodasz snu, iQuery nie będzie aktywne w czasie testu! (zajęło mi to kilka godzin,
zanim się
document.readyState == 'complete' && jQuery.active == 0działa świetnie, ale czy jest coś takiego w React? Ponieważ te kontrole nie zostaną złapane, nadal ładowanie danych reaguje / komponentu?
Rain9333
10

Czy biblioteka JS definiuje / inicjalizuje jakąś dobrze znaną zmienną w oknie?

Jeśli tak, możesz poczekać, aż zmienna się pojawi. Możesz użyć

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

aby przetestować ten warunek (na przykład window.SomeClass && window.SomeClass.variable != null:) i zwrócić wartość logiczną true/ false.

Zawiń to w a WebDriverWaiti poczekaj, aż skrypt powróci true.

Ashwin Prabhu
źródło
7

Miałem ten sam problem. To rozwiązanie działa dla mnie z 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

Roma Kap
źródło
Dziękuję Ci. Kod znajdujący się w linku działa dla mnie
ecamur,
7

Jeśli wszystko, co musisz zrobić, to poczekać, aż kod HTML na stronie ustabilizuje się, zanim spróbujesz wejść w interakcję z elementami, możesz okresowo sondować DOM i porównywać wyniki, jeśli DOM są takie same w podanym czasie ankiety, złoty. Coś takiego, w którym przekazujesz maksymalny czas oczekiwania i czas między ankietami strony przed porównaniem. Proste i skuteczne.

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;
        }
    }
}
TheFreddyKilo
źródło
1
Chciałbym tutaj zauważyć, że zapisałem czas potrzebny na sondowanie strony średniej wielkości dwa razy, a następnie porównanie tych ciągów w javie i zajęło to trochę ponad 20 ms.
TheFreddyKilo
2
Na początku stwierdziłem, że jest to brudne rozwiązanie, ale wydaje się, że działa świetnie i nie wymaga ustawiania zmiennych po stronie klienta. Jedna jaskinia może być stroną, która jest ciągle zmieniana przez javascript (tj. Zegar javascript), ale nie mam tego, więc działa!
Peter
4

Poniższy kod działa idealnie w moim przypadku - moja strona zawiera rozbudowane skrypty 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; 
   }   
  }
 }

Źródło - jak czekać na załadowanie / gotowość strony w Selenium WebDriver

user790049
źródło
2
Zamiast tych pętli powinieneś użyć selenu
wait
4

Można zastosować dwa warunki, aby sprawdzić, czy strona została załadowana przed znalezieniem dowolnego elementu na stronie:

WebDriverWait wait = new WebDriverWait(driver, 50);

Użycie poniższego readyState będzie czekać do załadowania strony

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

Poniżej JQuery będzie czekać, aż dane nie zostaną załadowane

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

Po tym JavaScriptCode spróbuj znaleźćOut webElement.

WebElement we = wait.until(ExpectedConditions.presenceOfElementLocated(by));
Ankit Gupta
źródło
1
Selenium również robi to domyślnie, doda to trochę dodatkowego czasu wykonania kodu do twojego skryptu (co może wystarczyć na załadowanie dodatkowych rzeczy, ale z drugiej strony może nie być)
Ardesco
2

Poprosiłem moich programistów, aby utworzyli zmienną JavaScript „isProcessing”, do której mam dostęp (w obiekcie „ae”), którą ustawiają, gdy wszystko zaczyna działać, i jest jasne, gdy wszystko jest gotowe. Następnie uruchamiam go w akumulatorze, który sprawdza go co 100 ms, aż osiągnie pięć z rzędu, co daje łącznie 500 ms bez żadnych zmian. Jeśli minie 30 sekund, zgłaszam wyjątek, ponieważ do tego czasu coś powinno się wydarzyć. To jest w 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.");
}
Roger Cook
źródło
1

Aby zrobić to poprawnie, musisz obsłużyć wyjątki.

Oto jak mam czekać na iFrame. Wymaga to, aby klasa testowa JUnit przekazała wystąpienie RemoteWebDriver do obiektu strony:

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

UWAGA: Tutaj możesz zobaczyć cały mój przykład roboczy .

djangofan
źródło
1

W przypadku nodejsbiblioteki Selenium użyłem następującego fragmentu. W moim przypadku, szukałem dwóch obiektów, które zostały dodane do okna, które w tym przykładzie <SOME PROPERTY>, 10000jest milisekund czasu oczekiwania, <NEXT STEP HERE>jest to, co dzieje się po właściwości znajdują się w oknie.

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);
});
Jason Lydon
źródło
0

Możesz napisać logikę, aby sobie z tym poradzić. Mam napisać metodę, która zwróci WebElementi ta metoda zostanie wywołana trzykrotnie lub możesz zwiększyć czas i dodać zerową kontrolę WebElementOto przykład

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;
        }
    }
Vaibhav Pandey
źródło
0

Oto z mojego własnego kodu:
Window.setTimeout jest wykonywany tylko wtedy, gdy przeglądarka jest bezczynna.
Tak więc rekurencyjne wywołanie funkcji (42 razy) zajmie 100 ms, jeśli przeglądarka nie jest aktywna, i dużo więcej, jeśli przeglądarka jest zajęta czymś innym.

    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;

Jako bonus, licznik można zresetować na document.readyState lub na wywołaniach jQuery Ajax lub jeśli są uruchomione animacje jQuery (tylko jeśli Twoja aplikacja używa jQuery do wywołań 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); " +
                            " } "+ 

...

EDYCJA: Zauważyłem, że executeAsyncScript nie działa dobrze, jeśli ładuje się nowa strona, a test może przestać odpowiadać w nieskończoność, lepiej zamiast tego użyć tego.

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");
        }
    };
}
Mariusz
źródło
0

Nie wiem, jak to zrobić, ale w moim przypadku koniec ładowania i renderowania strony pasuje do FAVICON wyświetlanego w zakładce Firefox.

Więc jeśli uda nam się pobrać obraz ikony ulubionych w przeglądarce internetowej, strona jest w pełni załadowana.

Ale jak to wykonać ...

Nolmë Informatique
źródło
-1

Oto jak to robię:

new WebDriverWait(driver, 20).until(
       ExpectedConditions.jsReturnsValue(
                   "return document.readyState === 'complete' ? true : false"));
Victor Trofimciuc
źródło
Selenium również robi to domyślnie, doda to trochę dodatkowego czasu wykonania kodu do twojego skryptu (co może wystarczyć na załadowanie dodatkowych rzeczy, ale z drugiej strony może nie być)
Ardesco
-1

Podoba mi się twój pomysł przeszukiwania kodu HTML, aż będzie stabilny. Mogę dodać to do własnego rozwiązania. Poniższe podejście jest w C # i wymaga jQuery.

Jestem programistą projektu testowego SuccessFactors (SaaS), w którym nie mamy żadnego wpływu na programistów ani na cechy DOM za stroną internetową. Produkt SaaS może potencjalnie zmienić swój podstawowy projekt DOM 4 razy w roku, więc trwają poszukiwania solidnych, wydajnych sposobów testowania z Selenium (w tym NIE testowania z Selenium, jeśli to możliwe!)

Oto, czego używam do określenia „gotowa strona”. Obecnie działa we wszystkich moich własnych testach. To samo podejście działało również w przypadku dużej wewnętrznej aplikacji internetowej Java kilka lat temu i było solidne przez ponad rok, gdy opuściłem projekt.

  • Driver to instancja WebDriver, która komunikuje się z przeglądarką
  • DefaultPageLoadTimeout jest wartością limitu czasu w taktach (100ns na takt)

public IWebDriver Driver { get; private set; }

// ...

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

W dalszej części zwróć uwagę na kolejność czekania w metodzie PageReady(dokument Selenium gotowy, Ajax, animacje), co ma sens, jeśli się nad tym zastanowić:

  1. załaduj stronę zawierającą kod
  2. użyj kodu, aby załadować dane z dowolnego miejsca za pośrednictwem Ajax
  3. przedstaw dane, ewentualnie z animacjami

Coś takiego, jak podejście do porównywania DOM, może być użyte między 1 a 2, aby dodać kolejną warstwę niezawodności.


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");
}
Nacięcie
źródło