Selenium WebDriver: Attendi il caricamento di una pagina complessa con JavaScript


103

Ho un'applicazione web da testare con Selenium. C'è molto JavaScript in esecuzione al caricamento della pagina.
Questo codice JavaScript non è scritto così bene ma non posso cambiare nulla. Quindi aspettare che un elemento appaia nel DOM con il findElement()metodo non è un'opzione.
Voglio creare una funzione generica in Java per attendere il caricamento di una pagina, una possibile soluzione sarebbe:

  • eseguire uno script JavaScript da WebDriver e memorizzare il risultato di document.body.innerHTMLin una variabile stringa body.
  • confronta la bodyvariabile con la versione precedente di body. se sono uguali allora impostare incrementare un contatore notChangedCountaltrimenti impostare notChangedCounta zero.
  • attendere un po 'di tempo (ad esempio 50 ms).
  • se la pagina non è cambiata per un po 'di tempo (500 ms ad esempio) notChangedCount >= 10quindi esci dal ciclo altrimenti passa al primo passaggio.

Pensi che sia una soluzione valida?


findElement () non aspetta - cosa intendi con questo?
Rostislav Matl

1
findElementattende che un elemento sia disponibile, ma a volte l'elemento è disponibile prima che il codice javascript sia inizializzato completamente, ecco perché non è un'opzione.
psadac

L'ho dimenticato: sono abituato a usare WebDriverWait e ExpectedCondition è molto più flessibile.
Rostislav Matl

Risposte:


73

Se qualcuno conoscesse davvero una risposta generale e sempre applicabile, sarebbe stata implementata ovunque secoli fa e avrebbe reso le nostre vite molto più facili.

Ci sono molte cose che puoi fare, ma ognuna di esse ha un problema:

  1. Come ha detto Ashwin Prabhu, se conosci bene la sceneggiatura, puoi osservarne il comportamento e tracciare alcune delle sue variabili su windowo documentecc. Questa soluzione, tuttavia, non è per tutti e può essere utilizzata solo da te e solo su un insieme limitato di pagine.

  2. La tua soluzione osservando il codice HTML e se è stato modificato o meno per un po 'di tempo non è male (inoltre, esiste un metodo per ottenere direttamente l'HTML originale e non modificato WebDriver), ma:

    • Ci vuole molto tempo per affermare effettivamente una pagina e potrebbe prolungare significativamente il test.
    • Non sai mai qual è l'intervallo giusto. Lo script potrebbe scaricare qualcosa di grande che richiede più di 500 ms. Ci sono diversi script sulla pagina interna della nostra azienda che richiedono diversi secondi in IE. Il tuo computer potrebbe essere temporaneamente a corto di risorse: supponiamo che un antivirus farà funzionare completamente la tua CPU, quindi 500 ms potrebbero essere troppo brevi anche per script non complessi.
    • Alcuni script non vengono mai eseguiti. Si chiamano con un po 'di delay ( setTimeout()) e lavorano ancora e ancora e potrebbero cambiare l'HTML ogni volta che vengono eseguiti. Scherzi a parte, ogni pagina "Web 2.0" lo fa. Anche Stack Overflow. Potresti sovrascrivere i metodi più comuni utilizzati e considerare gli script che li utilizzano come completati, ma ... non puoi esserne sicuro.
    • E se lo script fa qualcosa di diverso dalla modifica dell'HTML? Potrebbe fare migliaia di cose, non solo un po 'di innerHTMLdivertimento.
  3. Ci sono strumenti per aiutarti in questo. Vale a dire Progress Listeners insieme a nsIWebProgressListener e alcuni altri. Il supporto del browser per questo, tuttavia, è orribile. Firefox ha iniziato a provare a supportarlo da FF4 in poi (ancora in evoluzione), IE ha il supporto di base in IE9.

E credo che presto potrei trovare un'altra soluzione imperfetta. Il fatto è che non c'è una risposta definitiva su quando dire "ora la pagina è completa" a causa degli script eterni che fanno il loro lavoro. Scegli quello che ti serve meglio, ma fai attenzione ai suoi difetti.


31

Grazie Ashwin!

Nel mio caso dovrei aspettare l'esecuzione di un plugin jquery in qualche elemento .. specificamente "qtip"

in base al tuo suggerimento, ha funzionato perfettamente per me:

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

Nota: sto usando Webdriver 2


2
Funziona molto bene anche per me. Nel mio caso, un elemento è stato modificato da Javascript subito dopo il caricamento della pagina e Selenium non lo stava aspettando implicitamente.
Noxxys

2
Da dove viene Predicate? quale importazione ??
Amado Saladino

@AmadoSaladino "import com.google.common.base.Predicate;" dal link google lib guava-15.0: mvnrepository.com/artifact/com.google.guava/guava/15.0 ci sono la versione più recente 27.0 puoi provarla!
Eduardo Fabricio

26

È necessario attendere che Javascript e jQuery finiscano il caricamento. Esegui Javascript per verificare se jQuery.activeè 0ed document.readyStateè complete, il che significa che il caricamento di JS e jQuery è completo.

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
Non sono sicuro che funzioni in ogni circostanza, ma funziona a meraviglia per il mio caso d'uso
DaveH

1
Funziona anche per me, ma c'è una parte complicata: diciamo che la tua pagina è già caricata. Se interagisci con un elemento [es. Manda un .click () o .send alcune chiavi] e poi vuoi controllare quando la tua pagina ha terminato l'elaborazione devi aggiungere un ritardo: es. Click (), sleep (0,5 sec ), attendi fino a (readyState = 'complete' & jQuery.active == 0). Se non aggiungi il sonno, iQuery non sarà attivo al momento del test! (Mi ci sono volute alcune ore per scoprirlo, quindi ho pensato di condividerlo)
Fivos Vilanakis

document.readyState == 'complete' && jQuery.active == 0funziona alla grande ma c'è qualcosa del genere per React? Dal momento che quei controlli non catturano ancora il caricamento di dati / componenti di reazione?
Pioggia 9333



7

Se tutto ciò che devi fare è attendere che l'html sulla pagina diventi stabile prima di provare a interagire con gli elementi, puoi interrogare periodicamente il DOM e confrontare i risultati, se i DOM sono gli stessi entro il tempo di polling specificato, sei d'oro. Qualcosa del genere in cui passi il tempo di attesa massimo e il tempo tra i sondaggi delle pagine prima del confronto. Semplice ed efficace.

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
Vorrei sottolineare qui che ho registrato il tempo necessario per eseguire il polling di una pagina di medie dimensioni due volte, quindi ho confrontato quelle stringhe in java e ci sono voluti poco più di 20 ms.
TheFreddyKilo

2
All'inizio ho scoperto che questa è una soluzione sporca, ma sembra funzionare alla grande e non comporta l'impostazione di variabili lato client. Una caverna potrebbe essere una pagina che viene costantemente modificata da javascript (cioè un orologio javascript) ma non ce l'ho, quindi funziona!
Peter,

4

Il codice seguente funziona perfettamente nel mio caso: la mia pagina contiene script Java complessi

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

Sorgente - Come attendere che la pagina venga caricata / pronta in Selenium WebDriver


2
Dovresti usare le attese al selenio invece di quei loop
cocorossello

4

È possibile utilizzare due condizioni per verificare se la pagina è stata caricata prima di trovare qualsiasi elemento nella pagina:

WebDriverWait wait = new WebDriverWait(driver, 50);

L'utilizzo di readyState sotto aspetterà fino al caricamento della pagina

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

Sotto JQuery attenderà che i dati non siano stati caricati

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

Dopo questi JavaScriptCode prova a trovare WebElement.

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

1
Anche il selenio lo fa per impostazione predefinita, questo aggiungerà solo un po 'di tempo di esecuzione del codice aggiuntivo al tuo script (che potrebbe essere abbastanza tempo per caricare altre cose, ma poi di nuovo potrebbe non esserlo)
Ardesco

2

Ho chiesto ai miei sviluppatori di creare una variabile JavaScript "isProcessing" a cui posso accedere (nell'oggetto "ae") che impostano quando le cose iniziano a funzionare e che si cancellano quando le cose sono fatte. Quindi lo eseguo in un accumulatore che lo controlla ogni 100 ms fino a quando non ne ottiene cinque di fila per un totale di 500 ms senza alcuna modifica. Se passano 30 secondi, lancio un'eccezione perché a quel punto sarebbe dovuto succedere qualcosa. Questo è in 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

Per farlo correttamente, è necessario gestire le eccezioni.

Ecco come attendo un iFrame. Ciò richiede che la tua classe di test JUnit passi l'istanza di RemoteWebDriver nell'oggetto della pagina:

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

NOTA: puoi vedere il mio intero esempio di lavoro qui .


1

Per la nodejslibreria Selenium, ho usato il seguente frammento. Nel mio caso, stavo cercando due oggetti che vengono aggiunti alla finestra, che in questo esempio sono <SOME PROPERTY>, 10000è il timeout in millisecondi, <NEXT STEP HERE>è ciò che accade dopo che le proprietà sono state trovate sulla finestra.

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

Puoi scrivere della logica per gestire questo. Ho scritto un metodo che restituirà il WebElemente questo metodo verrà chiamato tre volte oppure puoi aumentare il tempo e aggiungere un controllo nullo per WebElementEcco un esempio

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

Ecco dal mio codice:
Window.setTimeout viene eseguito solo quando il browser è inattivo.
Quindi chiamare la funzione in modo ricorsivo (42 volte) richiederà 100 ms se non c'è attività nel browser e molto di più se il browser è occupato a fare qualcos'altro.

    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;

Come bonus il contatore può essere ripristinato su document.readyState o su chiamate jQuery Ajax o se sono in esecuzione animazioni jQuery (solo se la tua app utilizza jQuery per le chiamate 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: Ho notato che executeAsyncScript non funziona bene se viene caricata una nuova pagina e il test potrebbe smettere di rispondere indefinitamente, meglio usarlo invece.

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

Non so come farlo, ma nel mio caso, la fine del caricamento della pagina e il rendering corrispondono a FAVICON visualizzato nella scheda Firefox.

Quindi, se possiamo ottenere l'immagine della favicon nel browser web, la pagina web è completamente caricata.

Ma come eseguire questo ...



-1

Ecco come lo faccio:

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

Anche il selenio lo fa per impostazione predefinita, questo aggiungerà solo un po 'di tempo di esecuzione del codice aggiuntivo al tuo script (che potrebbe essere tempo sufficiente per caricare altre cose, ma poi di nuovo potrebbe non esserlo)
Ardesco

-1

Mi piace la tua idea di interrogare l'HTML finché non è stabile. Potrei aggiungerlo alla mia soluzione. Il seguente approccio è in C # e richiede jQuery.

Sono lo sviluppatore di un progetto di test SuccessFactors (SaaS) in cui non abbiamo alcuna influenza sugli sviluppatori o sulle caratteristiche del DOM dietro la pagina web. Il prodotto SaaS può potenzialmente cambiare il suo design DOM sottostante 4 volte l'anno, quindi la caccia è permanentemente attiva per metodi robusti e performanti per testare con selenio (incluso NON testare con selenio dove possibile!)

Ecco cosa uso per "pagina pronta". Attualmente funziona in tutti i miei test. Lo stesso approccio ha funzionato anche per una grande app web Java interna un paio di anni fa, ed era stato solido per oltre un anno quando ho lasciato il progetto.

  • Driver è l'istanza WebDriver che comunica con il browser
  • DefaultPageLoadTimeout è un valore di timeout in tick (100ns per tick)

public IWebDriver Driver { get; private set; }

// ...

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

In ciò che segue, nota l'ordine delle attese nel metodo PageReady(documento Selenium pronto, Ajax, animazioni), che ha senso se ci pensi:

  1. caricare la pagina contenente il codice
  2. usa il codice per caricare i dati da qualche parte tramite Ajax
  3. presentare i dati, possibilmente con animazioni

Qualcosa come il tuo approccio di confronto DOM potrebbe essere utilizzato tra 1 e 2 per aggiungere un altro livello di robustezza.


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");
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.