Selenium WebDriver: attendez que la page complexe avec JavaScript se charge

103

J'ai une application web à tester avec Selenium. Il y a beaucoup de JavaScript en cours d'exécution lors du chargement de la page.
Ce code JavaScript n'est pas si bien écrit mais je ne peux rien changer. Donc, attendre qu'un élément apparaisse dans le DOM avec findElement()method n'est pas une option.
Je souhaite créer une fonction générique en Java pour attendre le chargement d'une page, une solution possible serait:

  • exécutez un script JavaScript à partir de WebDriver et stockez le résultat de document.body.innerHTMLdans une variable chaîne body.
  • comparez la bodyvariable à la version précédente de body. s'ils sont identiques, incrémenter un compteur notChangedCountsinon mis notChangedCountà zéro.
  • attendez un peu (50 ms par exemple).
  • si la page n'a pas changé depuis un certain temps (500 ms par exemple) notChangedCount >= 10alors sortez de la boucle sinon bouclez au premier pas.

Pensez-vous que c'est une solution valable?

psadac
la source
findElement () n'attend pas - que voulez-vous dire par là?
Rostislav Matl
1
findElementattend qu'un élément soit disponible, mais parfois l'élément est disponible avant que le code javascript ne soit complètement initialisé, c'est pourquoi ce n'est pas une option.
psadac
Je l'ai oublié - j'ai l'habitude d'utiliser WebDriverWait et ExpectedCondition, c'est beaucoup plus flexible.
Rostislav Matl

Réponses:

73

Si quelqu'un connaissait réellement une réponse générale et toujours applicable, elle aurait été mise en œuvre partout il y a longtemps et rendrait nos vies tellement plus faciles.

Il y a beaucoup de choses que vous pouvez faire, mais chacune d'elles a un problème:

  1. Comme l'a dit Ashwin Prabhu, si vous connaissez bien le script, vous pouvez observer son comportement et suivre certaines de ses variables sur windowou documentetc. Cette solution, cependant, n'est pas pour tout le monde et ne peut être utilisée que par vous et uniquement sur un ensemble limité de pages.

  2. Votre solution en observant le code HTML et s'il a été ou non modifié depuis un certain temps n'est pas mal (il existe également une méthode pour obtenir le HTML d'origine et non édité directement par WebDriver), mais:

    • Cela prend beaucoup de temps pour affirmer une page et pourrait prolonger considérablement le test.
    • Vous ne savez jamais quel est le bon intervalle. Le script télécharge peut- être quelque chose de gros qui prend plus de 500 ms. Il y a plusieurs scripts sur la page interne de notre entreprise qui prennent plusieurs secondes dans IE. Votre ordinateur peut être temporairement à court de ressources - disons qu'un antivirus fera fonctionner pleinement votre processeur, alors 500 ms peuvent être trop courts, même pour des scripts non complexes.
    • Certains scripts ne sont jamais terminés. Ils s'appellent eux-mêmes avec un certain délai ( setTimeout()) et travaillent encore et encore et pourraient éventuellement changer le HTML à chaque fois qu'ils s'exécutent. Sérieusement, chaque page "Web 2.0" le fait. Même Stack Overflow. Vous pouvez remplacer les méthodes les plus courantes utilisées et considérer les scripts qui les utilisent comme terminés, mais ... vous ne pouvez pas en être sûr.
    • Et si le script fait autre chose que changer le HTML? Cela pourrait faire des milliers de choses, pas seulement du innerHTMLplaisir.
  3. Il existe des outils pour vous aider. À savoir Progress Listeners avec nsIWebProgressListener et quelques autres. Le support du navigateur pour cela, cependant, est horrible. Firefox a commencé à essayer de le supporter à partir de FF4 (toujours en évolution), IE a un support de base dans IE9.

Et je suppose que je pourrais bientôt trouver une autre solution défectueuse. Le fait est qu'il n'y a pas de réponse définitive sur le moment de dire "maintenant la page est terminée" à cause des scripts éternels qui font leur travail. Choisissez celui qui vous sert le mieux, mais méfiez-vous de ses lacunes.

Petr Janeček
la source
31

Merci Ashwin!

Dans mon cas, je devrais avoir besoin d'attendre une exécution du plugin jquery dans un élément .. spécifiquement "qtip"

basé sur votre indice, cela a parfaitement fonctionné pour moi:

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

Remarque: j'utilise Webdriver 2

Eduardo Fabricio
la source
2
Cela fonctionne très bien pour moi aussi. Dans mon cas, un élément a été modifié par Javascript juste après le chargement de la page, et Selenium n'attendait pas cela implicitement.
Noxxys
2
D'où vient Predicate? quelle importation ??
Amado Saladino
@AmadoSaladino "import com.google.common.base.Predicate;" à partir du lien google lib guava-15.0: mvnrepository.com/artifact/com.google.guava/guava/15.0 il existe une version plus récente 27.0, vous pouvez l'essayer!
Eduardo Fabricio
26

Vous devez attendre que Javascript et jQuery aient fini de se charger. Exécutez Javascript pour vérifier si jQuery.activeest 0et document.readyStateest complete, ce qui signifie que le chargement JS et jQuery est terminé.

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
la source
2
Je ne sais pas si cela fonctionnerait dans toutes les circonstances, mais cela fonctionne un régal pour mon cas d'utilisation
DaveH
1
Cela fonctionne pour moi aussi mais il y a une partie délicate: disons que votre page est déjà chargée. Si vous interagissez avec un élément [par exemple, envoyez un .click () ou .send quelques clés] et que vous voulez vérifier quand votre page a fini de traiter, vous devez ajouter un délai: par exemple, cliquez (), sleep (0.5 sec ), attendez que (readyState = 'complete' & jQuery.active == 0). Si vous n'ajoutez pas le sommeil, iQuery ne sera pas actif au moment du test! (il m'a fallu quelques heures pour le savoir, alors j'ai pensé à le partager)
Fivos Vilanakis
document.readyState == 'complete' && jQuery.active == 0fonctionne très bien mais y a-t-il quelque chose comme ça pour React? Puisque ces vérifications n'attraperont toujours pas le chargement des données / composants de réaction?
Rain9333
10

La bibliothèque JS définit / initialise-t-elle une variable bien connue dans la fenêtre?

Si c'est le cas, vous pouvez attendre que la variable apparaisse. Vous pouvez utiliser

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

pour tester cette condition (quelque chose comme window.SomeClass && window.SomeClass.variable != null:) et renvoyer un booléen true/ false.

Enveloppez ceci dans un WebDriverWait, et attendez que le script revienne true.

Ashwin Prabhu
la source
7

J'ai eu le même problème. Cette solution fonctionne pour moi à partir de 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
la source
Merci. Le code qui est dans le lien fonctionne pour moi,
ecamur
7

Si tout ce que vous avez à faire est d'attendre que le html sur la page devienne stable avant d'essayer d'interagir avec les éléments, vous pouvez interroger le DOM périodiquement et comparer les résultats, si les DOM sont les mêmes dans le temps d'interrogation donné, vous êtes d'or. Quelque chose comme ça où vous passez dans le temps d'attente maximum et le temps entre les sondages de page avant de comparer. Simple et 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;
        }
    }
}
TheFreddyKilo
la source
1
Je voudrais noter ici que j'ai enregistré le temps nécessaire pour interroger une page de taille moyenne deux fois, puis comparer ces chaînes en java et cela a pris un peu plus de 20 ms.
TheFreddyKilo
2
Au début, j'ai trouvé que c'était une solution sale, mais cela semble fonctionner très bien et cela n'implique pas de définir des variables côté client. Un caveit pourrait être une page qui est constamment modifiée par javascript (c'est-à-dire une horloge javascript) mais je ne l'ai pas donc ça marche!
Peter
4

Le code ci-dessous fonctionne parfaitement dans mon cas - ma page contient des scripts Java complexes

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

Source - Comment attendre que la page soit chargée / prête dans Selenium WebDriver

user790049
la source
2
Vous devriez utiliser des attentes au sélénium au lieu de ces boucles
cocorossello
4

Deux conditions peuvent être utilisées pour vérifier si la page est chargée avant de trouver un élément sur la page:

WebDriverWait wait = new WebDriverWait(driver, 50);

L'utilisation de readyState ci-dessous attendra le chargement de la page

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

Sous JQuery attendra que les données n'aient pas été chargées

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

Après ces JavaScriptCode, essayez de findOut webElement.

WebElement we = wait.until(ExpectedConditions.presenceOfElementLocated(by));
Ankit Gupta
la source
1
Selenium fait également cela par défaut, cela ajoutera simplement du temps supplémentaire d'exécution de code à votre script (ce qui peut être assez de temps pour que des choses supplémentaires se chargent, mais là encore peut ne pas l'être)
Ardesco
2

J'ai demandé à mes développeurs de créer une variable JavaScript "isProcessing" à laquelle je peux accéder (dans l'objet "ae") qu'ils définissent lorsque les choses commencent à fonctionner et qu'elles s'affichent lorsque les choses sont terminées. Je l'exécute ensuite dans un accumulateur qui le vérifie toutes les 100 ms jusqu'à ce qu'il en obtienne cinq de suite pour un total de 500 ms sans aucun changement. Si 30 secondes passent, je lance une exception car quelque chose aurait dû se passer d'ici là. C'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
la source
1

Pour le faire correctement, vous devez gérer les exceptions.

Voici comment j'attends un iFrame. Cela nécessite que votre classe de test JUnit transmette l'instance de RemoteWebDriver dans l'objet de page:

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

REMARQUE: vous pouvez voir mon exemple de travail complet ici .

Djangofan
la source
1

Pour la nodejsbibliothèque Selenium, j'ai utilisé l'extrait suivant. Dans mon cas, je cherchais deux objets qui sont ajoutés à la fenêtre, dans cet exemple sont <SOME PROPERTY>, 10000est le nombre de millisecondes de délai d'attente, <NEXT STEP HERE>est ce qui se passe après les propriétés se trouvent sur la fenêtre.

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
la source
0

Vous pouvez écrire une logique pour gérer cela. J'ai écrit une méthode qui renverra le WebElementet cette méthode sera appelée trois fois ou vous pouvez augmenter le temps et ajouter une vérification nulle pour WebElementVoici un exemple

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
la source
0

Voici mon propre code:
Window.setTimeout ne s'exécute que lorsque le navigateur est inactif.
Donc, appeler la fonction de manière récursive (42 fois) prendra 100 ms s'il n'y a aucune activité dans le navigateur et bien plus encore si le navigateur est occupé à faire autre chose.

    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;

En prime, le compteur peut être réinitialisé sur document.readyState ou sur les appels jQuery Ajax ou si des animations jQuery sont en cours d'exécution (uniquement si votre application utilise jQuery pour les appels 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: Je remarque que executeAsyncScript ne fonctionne pas bien si une nouvelle page se charge et que le test peut cesser de répondre indéfiniment, mieux vaut l'utiliser à la place.

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
la source
0

Je ne sais pas comment faire cela mais dans mon cas, le chargement de la fin de la page et le rendu correspondent à FAVICON affiché dans l'onglet Firefox.

Donc, si nous pouvons obtenir l'image favicon dans le navigateur Web, la page Web est entièrement chargée.

Mais comment faire ça ...

Nolmë Informatique
la source
-1

Voici comment je fais:

new WebDriverWait(driver, 20).until(
       ExpectedConditions.jsReturnsValue(
                   "return document.readyState === 'complete' ? true : false"));
Victor Trofimciuc
la source
Selenium fait également cela par défaut, cela ajoutera simplement du temps supplémentaire d'exécution de code à votre script (ce qui peut être assez de temps pour que des choses supplémentaires se chargent, mais là encore peut ne pas l'être)
Ardesco
-1

J'aime votre idée d'interroger le HTML jusqu'à ce qu'il soit stable. Je peux ajouter cela à ma propre solution. L'approche suivante est en C # et nécessite jQuery.

Je suis le développeur d'un projet de test SuccessFactors (SaaS) où nous n'avons aucune influence sur les développeurs ou les caractéristiques du DOM derrière la page Web. Le produit SaaS peut potentiellement changer sa conception DOM sous-jacente 4 fois par an, de sorte que la recherche est en permanence de moyens robustes et performants de tester avec Selenium (y compris NE PAS tester avec Selenium lorsque cela est possible!)

Voici ce que j'utilise pour «page prête». Cela fonctionne dans tous mes propres tests actuellement. La même approche a également fonctionné pour une grande application Web Java interne il y a quelques années, et avait été robuste pendant plus d'un an au moment où j'ai quitté le projet.

  • Driver est l'instance WebDriver qui communique avec le navigateur
  • DefaultPageLoadTimeout est une valeur de timeout en ticks (100ns par 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();

Dans ce qui suit, notez l'ordre des attentes dans la méthode PageReady(document Selenium prêt, Ajax, animations), ce qui a du sens si vous y réfléchissez:

  1. charger la page contenant le code
  2. utilisez le code pour charger les données de quelque part via Ajax
  3. présenter les données, éventuellement avec des animations

Quelque chose comme votre approche de comparaison DOM pourrait être utilisé entre 1 et 2 pour ajouter une autre couche de robustesse.


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