Selenium WebDriver: espere a que se cargue la página compleja con JavaScript

102

Tengo una aplicación web para probar con Selenium. Hay mucho JavaScript ejecutándose al cargar la página.
Este código JavaScript no está tan bien escrito, pero no puedo cambiar nada. Entonces, esperar a que aparezca un elemento en el DOM con el findElement()método no es una opción.
Quiero crear una función genérica en Java para esperar a que se cargue una página, una posible solución sería:

  • ejecute un script de JavaScript desde WebDriver y almacene el resultado de document.body.innerHTMLen una variable de cadena body.
  • compare la bodyvariable con la versión anterior de body. si son iguales, configure el incremento de un contador; de lo notChangedCountcontrario, establezca el valor notChangedCountcero.
  • espere un poco (50 ms por ejemplo).
  • Si la página no ha cambiado durante algún tiempo (500 ms por ejemplo) notChangedCount >= 10, salga del bucle; de ​​lo contrario, continúe con el primer paso.

¿Crees que es una solución válida?

psadac
fuente
findElement () no espera, ¿qué quieres decir con eso?
Rostislav Matl
1
findElementespera a que un elemento esté disponible, pero a veces el elemento está disponible antes de que el código javascript se inicialice por completo, por eso no es una opción.
psadac
Lo olvidé, estoy acostumbrado a usar WebDriverWait y ExpectedCondition, es mucho más flexible.
Rostislav Matl

Respuestas:

73

Si alguien realmente supiera una respuesta general y siempre aplicable, se habría implementado en todas partes hace siglos y nos haría la vida mucho más fácil.

Hay muchas cosas que puede hacer, pero cada una de ellas tiene un problema:

  1. Como dijo Ashwin Prabhu, si conoces bien el guión, se puede observar su comportamiento y realizar un seguimiento de algunas de sus variables en windowo documentetc. Esta solución, sin embargo, no es para todos y puede ser utilizado sólo por usted y sólo en un número limitado de páginas.

  2. Su solución al observar el código HTML y si se ha cambiado o no durante algún tiempo no está mal (también, hay un método para obtener el HTML original y no editado directamente por WebDriver), pero:

    • Se necesita mucho tiempo para afirmar realmente una página y podría prolongar la prueba de manera significativa.
    • Nunca se sabe cuál es el intervalo correcto. El script puede estar descargando algo grande que tarda más de 500 ms. Hay varios scripts en la página interna de nuestra empresa que tardan varios segundos en IE. Es posible que su computadora tenga pocos recursos temporalmente; digamos que un antivirus hará que su CPU funcione completamente, entonces 500 ms pueden ser demasiado cortos incluso para scripts no complejos.
    • Algunos guiones nunca se terminan. Se llaman a sí mismos con cierto retraso ( setTimeout()) y funcionan una y otra vez y posiblemente podrían cambiar el HTML cada vez que se ejecutan. En serio, todas las páginas de la "Web 2.0" lo hacen. Incluso Stack Overflow. Puede sobrescribir los métodos más comunes utilizados y considerar los scripts que los usan como completados, pero ... no puede estar seguro.
    • ¿Qué pasa si el script hace algo más que cambiar el HTML? Podría hacer miles de cosas, no solo innerHTMLdivertirse.
  3. Hay herramientas para ayudarte en esto. Es decir, Progress Listeners junto con nsIWebProgressListener y algunos otros. El soporte del navegador para esto, sin embargo, es horrible. Firefox comenzó a intentar admitirlo desde FF4 en adelante (aún en evolución), IE tiene soporte básico en IE9.

Y creo que pronto se me ocurrirá otra solución defectuosa. El hecho es que no hay una respuesta definitiva sobre cuándo decir "ahora la página está completa" debido a que los eternos guiones están haciendo su trabajo. Elija el que mejor le sirva, pero tenga cuidado con sus defectos.

Petr Janeček
fuente
31

¡Gracias Ashwin!

En mi caso, debería esperar a que se ejecute un complemento jquery en algún elemento ... específicamente "qtip"

según tu sugerencia, funcionó perfectamente para mí:

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

Nota: estoy usando Webdriver 2

Eduardo Fabricio
fuente
2
A mí también me funciona muy bien. En mi caso, un elemento fue modificado por Javascript justo después de que la página terminó de cargarse, y Selenium no estaba esperando eso implícitamente.
Noxxys
2
¿De dónde viene el predicado? que importa ??
Amado Saladino
@AmadoSaladino "import com.google.common.base.Predicate;" de google lib guava-15.0 enlace: mvnrepository.com/artifact/com.google.guava/guava/15.0 hay una versión más reciente 27.0, ¡puedes probarla!
Eduardo Fabricio
26

Debe esperar a que Javascript y jQuery terminen de cargarse. Ejecute Javascript para verificar si jQuery.activees 0y document.readyStatees complete, lo que significa que la carga de JS y jQuery está completa.

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
fuente
2
No estoy seguro de si esto funcionaría en todas las circunstancias, pero funciona de
maravilla
1
A mí también me funciona, pero hay una parte complicada: digamos que tu página ya está cargada. Si interactúa con un elemento [por ejemplo, envía un .click () o. Envía algunas claves] y luego quiere verificar cuando su página ha terminado de procesarse, necesita agregar un retraso: por ejemplo, haga clic en (), dormir (0.5 seg ), espere hasta (readyState = 'complete' & jQuery.active == 0). Si no agrega el sueño, iQuery no estará activo en el momento de la prueba. (me tomó algunas horas averiguarlo, así que pensé en compartirlo)
Fivos Vilanakis
document.readyState == 'complete' && jQuery.active == 0funciona muy bien, pero ¿hay algo así para React? ¿Dado que esas comprobaciones no detectan la carga de datos / componentes de reacción?
Rain9333
7

Tuve el mismo problema. Esta solución me funciona desde 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
fuente
gracias. El código que está en el enlace funciona para mí,
ecamur
7

Si todo lo que necesita hacer es esperar a que el html en la página se estabilice antes de intentar interactuar con los elementos, puede sondear el DOM periódicamente y comparar los resultados, si los DOM son los mismos dentro del tiempo de encuesta dado, está dorado. Algo como esto en el que pasa el tiempo de espera máximo y el tiempo entre encuestas de página antes de comparar. Simple y eficaz.

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
fuente
1
Me gustaría señalar aquí que registré el tiempo que tardé en sondear una página de tamaño mediano dos veces, luego comparé esas cadenas en Java y tardé un poco más de 20 ms.
TheFreddyKilo
2
Al principio encontré que esta era una solución sucia, pero parece funcionar muy bien y no implica configurar variables del lado del cliente. Una advertencia podría ser una página que javascript cambia constantemente (es decir, un reloj javascript), ¡pero no tengo eso, así que funciona!
Peter
4

El siguiente código funciona perfectamente en mi caso: mi página contiene scripts complejos de 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; 
   }   
  }
 }

Fuente: cómo esperar a que la página se cargue / esté lista en Selenium WebDriver

user790049
fuente
2
Deberías usar selenio esperas en lugar de esos bucles
cocorossello
4

Se pueden usar dos condiciones para verificar si la página está cargada antes de encontrar cualquier elemento en la página:

WebDriverWait wait = new WebDriverWait(driver, 50);

El uso de readyState a continuación esperará hasta que se cargue la página

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

A continuación, JQuery esperará hasta que no se hayan cargado los datos

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

Después de estos JavaScriptCode, intente findOut webElement.

WebElement we = wait.until(ExpectedConditions.presenceOfElementLocated(by));
Ankit Gupta
fuente
1
Selenium también hace esto de forma predeterminada, esto solo agregará algo de tiempo de ejecución de código adicional a su script (que puede ser suficiente tiempo para que se carguen cosas adicionales, pero de nuevo puede que no lo sea)
Ardesco
2

Les pedí a mis desarrolladores que crearan una variable de JavaScript "isProcessing" a la que pueda acceder (en el objeto "ae") que configuran cuando las cosas comienzan a funcionar y se aclaran cuando las cosas están hechas. Luego lo ejecuto en un acumulador que lo verifica cada 100 ms hasta que obtiene cinco en una fila para un total de 500 ms sin ningún cambio. Si pasan 30 segundos, lanzo una excepción porque algo debería haber sucedido para entonces. Esto está en 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
fuente
1

Para hacerlo correctamente, debe manejar las excepciones.

Así es como hago una espera para un iFrame. Esto requiere que su clase de prueba JUnit pase la instancia de RemoteWebDriver al objeto de página:

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: Puede ver mi ejemplo de trabajo completo aquí .

djangofan
fuente
1

Para la nodejsbiblioteca de Selenium, utilicé el siguiente fragmento. En mi caso, estaba buscando dos objetos que se agregan a la ventana, que en este ejemplo son <SOME PROPERTY>, 10000es el tiempo de espera en milisegundos, <NEXT STEP HERE>es lo que sucede después de que se encuentran las propiedades en la ventana.

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
fuente
0

Puedes escribir algo de lógica para manejar esto. Escribí un método que devolverá el WebElementy este método se llamará tres veces o puede aumentar el tiempo y agregar una verificación nula para WebElementAquí hay un ejemplo

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
fuente
0

Aquí está mi propio código:
Window.setTimeout se ejecuta solo cuando el navegador está inactivo.
Entonces, llamar a la función de manera recursiva (42 veces) tomará 100 ms si no hay actividad en el navegador y mucho más si el navegador está ocupado haciendo otra cosa.

    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;

Como beneficio adicional, el contador se puede restablecer en document.readyState o en llamadas jQuery Ajax o si se están ejecutando animaciones jQuery (solo si su aplicación usa jQuery para llamadas 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); " +
                            " } "+ 

...

EDITAR: Noto que executeAsyncScript no funciona bien si se carga una nueva página y la prueba puede dejar de responder indefinidamente, es mejor usar esto en su lugar.

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
fuente
0

No sé cómo hacer eso, pero en mi caso, la carga y el renderizado del final de la página coinciden con FAVICON que se muestra en la pestaña de Firefox.

Entonces, si podemos obtener la imagen del favicon en el navegador web, la página web está completamente cargada.

Pero, ¿cómo realizar esto ...

Nolmë Informatique
fuente
-1

Así es como lo hago:

new WebDriverWait(driver, 20).until(
       ExpectedConditions.jsReturnsValue(
                   "return document.readyState === 'complete' ? true : false"));
Víctor Trofimciuc
fuente
Selenium también hace esto de forma predeterminada, esto solo agregará algo de tiempo de ejecución de código adicional a su script (que puede ser suficiente tiempo para que se carguen cosas adicionales, pero nuevamente puede que no lo sea)
Ardesco
-1

Me gusta tu idea de sondear el HTML hasta que sea estable. Puedo agregar eso a mi propia solución. El siguiente enfoque está en C # y requiere jQuery.

Soy el desarrollador de un proyecto de prueba de SuccessFactors (SaaS) donde no tenemos ninguna influencia sobre los desarrolladores o las características del DOM detrás de la página web. El producto SaaS puede cambiar potencialmente su diseño DOM subyacente 4 veces al año, por lo que la búsqueda está permanentemente activa de formas sólidas y eficaces de realizar pruebas con Selenium (¡incluyendo NO realizar pruebas con Selenium cuando sea posible!)

Esto es lo que uso para "página lista". Actualmente funciona en todas mis propias pruebas. El mismo enfoque también funcionó para una gran aplicación web interna de Java hace un par de años, y había sido sólido durante más de un año cuando dejé el proyecto.

  • Driver es la instancia de WebDriver que se comunica con el navegador
  • DefaultPageLoadTimeout es un valor de tiempo de espera en ticks (100ns por 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();

A continuación, observe el orden de las esperas en el método PageReady(documento de Selenium listo, Ajax, animaciones), lo que tiene sentido si lo piensa:

  1. cargar la página que contiene el código
  2. use el código para cargar los datos desde algún lugar a través de Ajax
  3. presentar los datos, posiblemente con animaciones

Algo como su enfoque de comparación DOM podría usarse entre 1 y 2 para agregar otra capa de robustez.


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