Selenium WebDriver: aguarde o carregamento de uma página complexa com JavaScript

102

Tenho um aplicativo da web para testar com Selenium. Há muito JavaScript em execução no carregamento da página.
Este código JavaScript não é tão bem escrito, mas não posso mudar nada. Portanto, esperar que um elemento apareça no DOM com o findElement()método não é uma opção.
Quero criar uma função genérica em Java para aguardar o carregamento de uma página, uma possível solução seria:

  • execute um script JavaScript do WebDriver e armazene o resultado document.body.innerHTMLem uma variável de string body.
  • compare a bodyvariável com a versão anterior de body. se eles forem iguais, defina o incremento de um contador, notChangedCountcaso contrário, defina notChangedCountcomo zero.
  • espere um pouco (50 ms, por exemplo).
  • se a página não mudou por algum tempo (500 ms, por exemplo), notChangedCount >= 10então saia do loop, caso contrário, volte para a primeira etapa.

Você acha que é uma solução válida?

psadac
fonte
findElement () não espera - o que você quer dizer com isso?
Rostislav Matl
1
findElementespera que um elemento esteja disponível, mas às vezes o elemento está disponível antes que o código javascript seja inicializado completamente, por isso não é uma opção.
psadac
Eu esqueci - estou acostumado a usar WebDriverWait e ExpectedCondition, é muito mais flexível.
Rostislav Matl

Respostas:

73

Se alguém realmente soubesse uma resposta geral e sempre aplicável, ela teria sido implementada em todos os lugares há muito tempo e tornaria nossas vidas MUITO mais fáceis.

Há muitas coisas que você pode fazer, mas cada uma delas tem um problema:

  1. Como Ashwin Prabhu disse, se você conhece bem o script, pode observar seu comportamento e rastrear algumas de suas variáveis ​​em windowou documentetc. Esta solução, no entanto, não é para todos e pode ser usada apenas por você e apenas em um conjunto limitado de Páginas.

  2. Sua solução, observando o código HTML e se ele foi ou não alterado por algum tempo, não é ruim (também, há um método para obter o HTML original e não editado diretamente WebDriver), mas:

    • Demora muito para afirmar uma página e pode prolongar significativamente o teste.
    • Você nunca sabe qual é o intervalo certo. O script pode estar baixando algo grande que leva mais de 500 ms. Existem vários scripts na página interna da nossa empresa que demoram vários segundos no IE. Seu computador pode estar temporariamente com poucos recursos - digamos que um antivírus fará sua CPU funcionar totalmente, então 500 ms pode ser muito curto, mesmo para scripts não complexos.
    • Alguns scripts nunca são concluídos. Eles se autodenominam com algum atraso ( setTimeout()) e trabalham continuamente e podem alterar o HTML toda vez que são executados. Sério, toda página "Web 2.0" faz isso. Até Stack Overflow. Você poderia sobrescrever os métodos mais comuns usados ​​e considerar os scripts que os usam como concluídos, mas ... você não pode ter certeza.
    • E se o script fizer algo diferente de alterar o HTML? Ele poderia fazer milhares de coisas, não apenas innerHTMLdiversão.
  3. Existem ferramentas para ajudá-lo nisso. Ou seja, Progress Listeners juntamente com nsIWebProgressListener e alguns outros. O suporte do navegador para isso, no entanto, é horrível. O Firefox começou a tentar dar suporte a partir do FF4 (ainda em evolução), o IE tem suporte básico no IE9.

E acho que posso encontrar outra solução falha em breve. O fato é - não há uma resposta definitiva sobre quando dizer "agora a página está completa" por causa dos scripts eternos fazendo seu trabalho. Escolha aquele que melhor atende a você, mas cuidado com suas deficiências.

Petr Janeček
fonte
31

Obrigado Ashwin!

No meu caso, devo esperar a execução de um plugin jquery em algum elemento .. especificamente "qtip"

com base na sua dica, funcionou perfeitamente para mim:

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

Observação: estou usando o Webdriver 2

Eduardo fabricio
fonte
2
Funciona muito bem para mim também. No meu caso, um elemento foi modificado por Javascript logo após a página terminar de carregar, e o Selenium não estava esperando por isso implicitamente.
Noxxys
2
De onde vem o Predicate? qual importação ??
Amado Saladino
@AmadoSaladino "import com.google.common.base.Predicate;" do google lib guava-15.0 link: mvnrepository.com/artifact/com.google.guava/guava/15.0 existem versões mais recentes 27.0 que você pode tentar!
Eduardo Fabricio
26

Você precisa esperar que o Javascript e o jQuery terminem de carregar. Execute o Javascript para verificar se jQuery.activeé 0e document.readyStateé complete, o que significa que o carregamento de JS e jQuery está 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);
}
LINGS
fonte
2
Não tenho certeza se isso funcionaria em todas as circunstâncias, mas funciona bem para o meu caso de uso
DaveH
1
Funciona comigo também, mas há uma parte complicada: digamos que sua página já esteja carregada. Se você interagir com um elemento [por exemplo, enviar um .click () ou .enviar algumas chaves para ele] e então quiser verificar quando o processamento de sua página termina, você precisa adicionar um atraso: por exemplo, clique (), hibernar (0,5 segundos ), espere até (readyState = 'complete' & jQuery.active == 0). Se você não adicionar o sleep, o iQuery não estará ativo no momento do teste! (demorei algumas horas para descobrir, então pensei em compartilhá-lo)
Fivos Vilanakis
document.readyState == 'complete' && jQuery.active == 0funciona muito bem, mas há algo assim para React? Uma vez que essas verificações não travarão o carregamento de dados / componentes de reação?
Rain9333
10

A biblioteca JS define / inicializa alguma variável conhecida na janela?

Nesse caso, você pode esperar que a variável apareça. Você pode usar

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

para testar essa condição (algo como window.SomeClass && window.SomeClass.variable != null:) e retornar um booleano true/ false.

Envolva isso em um WebDriverWaite espere até que o script retorne true.

Ashwin Prabhu
fonte
7

Eu tive o mesmo problema. Esta solução funciona para mim do 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
fonte
obrigado. O código que está no link funciona para mim,
ecamur
7

Se tudo o que você precisa fazer é esperar que o html na página se torne estável antes de tentar interagir com os elementos, você pode pesquisar o DOM periodicamente e comparar os resultados. Se os DOMs forem os mesmos dentro do tempo de pesquisa determinado, você dourado. Algo assim onde você passa o tempo máximo de espera e o tempo entre as pesquisas de página antes de comparar. Simples e 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
fonte
1
Gostaria de observar aqui que registrei o tempo que levou para pesquisar uma página de tamanho médio duas vezes, em seguida, comparei essas strings em java e levou um pouco mais de 20 ms.
TheFreddyKilo
2
No começo eu achei que essa solução era suja, mas parece funcionar muito bem e não envolve definir variáveis ​​do lado do cliente. Um caveit pode ser uma página que está constantemente sendo alterada por javascript (ou seja, um relógio javascript), mas eu não tenho isso, então funciona!
Peter
4

O código abaixo funciona perfeitamente no meu caso - minha página contém scripts java complexos

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

Fonte - Como esperar que a página carregue / pronta no Selenium WebDriver

user790049
fonte
2
Você deve usar esperas de selênio em vez desses loops
cocorossello
4

Duas condições podem ser usadas para verificar se a página foi carregada antes de encontrar qualquer elemento na página:

WebDriverWait wait = new WebDriverWait(driver, 50);

Usar o readyState abaixo irá esperar até o carregamento da página

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

Abaixo do JQuery irá esperar até que os dados não sejam carregados

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

Após esses JavaScriptCode, tente encontrarOut webElement.

WebElement we = wait.until(ExpectedConditions.presenceOfElementLocated(by));
Ankit Gupta
fonte
1
Selenium também faz isso por padrão, isso apenas adicionará algum tempo de execução de código adicional ao seu script (que pode ser tempo suficiente para coisas adicionais carregar, mas, novamente, pode não ser)
Ardesco
2

Pedi aos meus desenvolvedores para criar uma variável JavaScript "isProcessing" que eu possa acessar (no objeto "ae") que eles definem quando as coisas começam a funcionar e limpa quando as coisas são feitas. Em seguida, executo-o em um acumulador que o verifica a cada 100 ms até que ele obtenha cinco em uma linha para um total de 500 ms sem nenhuma alteração. Se 30 segundos se passarem, lanço uma exceção porque algo deveria ter acontecido até lá. Isso está em 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
fonte
1

Para fazer isso corretamente, você precisa lidar com as exceções.

Aqui está como eu faço uma espera por um iframe. Isso requer que sua classe de teste JUnit passe a instância de RemoteWebDriver para o 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: Você pode ver todo o meu exemplo de trabalho aqui .

Djangofan
fonte
1

Para a nodejsbiblioteca Selenium, usei o seguinte trecho. No meu caso, eu estava procurando por dois objetos que são adicionados à janela, que neste exemplo são <SOME PROPERTY>, 10000é o tempo limite em milissegundos, <NEXT STEP HERE>é o que acontece depois que as propriedades são encontradas na janela.

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

Você pode escrever alguma lógica para lidar com isso. Eu escrevi um método que retornará o WebElemente este método será chamado três vezes ou você pode aumentar o tempo e adicionar uma verificação nula para WebElementAqui está um exemplo

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

Aqui está o meu próprio código:
Window.setTimeout executa somente quando o navegador está ocioso.
Portanto, chamar a função recursivamente (42 vezes) levará 100 ms se não houver atividade no navegador e muito mais se o navegador estiver ocupado fazendo outra coisa.

    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 bônus, o contador pode ser redefinido em document.readyState ou em chamadas jQuery Ajax ou se alguma animação jQuery estiver em execução (apenas se seu aplicativo usar jQuery para chamadas 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: Percebi que executeAsyncScript não funciona bem se uma nova página for carregada e o teste pode parar de responder indefinidamente, melhor usar isso em vez disso.

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

Não sei como fazer isso, mas no meu caso, o carregamento e renderização do final da página correspondem ao FAVICON exibido na guia Firefox.

Portanto, se conseguirmos obter a imagem favicon no navegador da web, a página da web estará totalmente carregada.

Mas como fazer isso ....

Nolmë Informatique
fonte
-1

É assim que eu faço:

new WebDriverWait(driver, 20).until(
       ExpectedConditions.jsReturnsValue(
                   "return document.readyState === 'complete' ? true : false"));
Victor Trofimciuc
fonte
O Selenium também faz isso por padrão, isso apenas adicionará algum tempo de execução de código adicional ao seu script (que pode ser tempo suficiente para coisas adicionais carregar, mas, novamente, pode não ser)
Ardesco
-1

Gosto da sua ideia de pesquisar o HTML até que esteja estável. Posso adicionar isso à minha própria solução. A abordagem a seguir está em C # e requer jQuery.

Sou o desenvolvedor de um projeto de teste SuccessFactors (SaaS) em que não temos nenhuma influência sobre os desenvolvedores ou as características do DOM por trás da página da web. O produto SaaS pode alterar potencialmente seu design de DOM subjacente 4 vezes por ano, de modo que a busca é permanente por maneiras robustas e de alto desempenho de testar com Selenium (incluindo NÃO testar com Selenium quando possível!)

Aqui está o que eu uso para "página pronta". Ele funciona em todos os meus próprios testes atualmente. A mesma abordagem também funcionou para um grande aplicativo da web Java interno alguns anos atrás, e já era robusta por mais de um ano na época em que deixei o projeto.

  • Driver é a instância do WebDriver que se comunica com o navegador
  • DefaultPageLoadTimeout é um valor de tempo limite em 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 seguir, observe a ordem das esperas no método PageReady(documento Selenium pronto, Ajax, animações), o que faz sentido se você pensar bem:

  1. carregue a página que contém o código
  2. use o código para carregar os dados de algum lugar via Ajax
  3. apresentar os dados, possivelmente com animações

Algo como a abordagem de comparação do DOM pode ser usado entre 1 e 2 para adicionar outra camada 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");
}
usuario
fonte