web-dev-qa-db-fra.com

Comprendre l'exécution d'un script asynchrone dans Selenium

J'utilise Selenium (avec liaisons python et via protractor principalement) depuis assez longtemps et à chaque fois que je devais exécuter un code javascript, j'ai utilisé la méthode execute_script(). Par exemple, pour faire défiler la page (python):

driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

Ou, pour défilement infini à l'intérieur d'un autre élément (rapporteur):

var div = element(by.css('div.table-scroll'));
var lastRow = element(by.css('table#myid tr:last-of-type'));

browser.executeScript("return arguments[0].offsetTop;", lastRow.getWebElement()).then(function (offset) {
    browser.executeScript('arguments[0].scrollTop = arguments[1];', div.getWebElement(), offset).then(function() {
        // assertions

    });
});

Ou, pour obtenir un dictionnaire de tous les attributs des éléments (python):

driver.execute_script('var items = {}; for (index = 0; index < arguments[0].attributes.length; ++index) { items[arguments[0].attributes[index].name] = arguments[0].attributes[index].value }; return items;', element)

Mais, l'API WebDriver a également execute_async_script() que je n'ai pas personnellement utilisé.

Quels cas d'utilisation couvre-t-il? Quand dois-je utiliser execute_async_script() au lieu de la fonction régulière execute_script()?

La question est spécifique au sélénium, mais indépendante de la langue.

22
alecxe

Voici le référence aux deux API (enfin c'est Javadoc, mais les fonctions sont les mêmes), et en voici un extrait qui met en évidence la différence

[executeAsyncScript] Exécute un morceau asynchrone de JavaScript dans le contexte du cadre ou de la fenêtre actuellement sélectionné. Contrairement à l'exécution de JavaScript synchrone, les scripts exécutés avec cette méthode doivent explicitement signaler qu'ils sont terminés en appelant le rappel fourni. Ce rappel est toujours injecté dans la fonction exécutée comme dernier argument.

Fondamentalement, execSync bloque les autres actions effectuées par le navigateur Selenium, tandis que execAsync ne bloque pas et appelle un callback lorsqu'il est terminé.


Puisque vous avez travaillé avec le rapporteur, je vais utiliser cela comme exemple. Le rapporteur utilise executeAsyncScript dans les deux get et waitForAngular

Dans waitForAngular, le rapporteur doit attendre jusqu'à angular annonce que tous les événements sont réglés. Vous ne pouvez pas utiliser executeScript car cela doit renvoyer une valeur à la fin (bien que je suppose que vous pouvez implémenter une boucle occupée qui interroge angular constamment jusqu'à ce qu'il soit fait). La façon dont cela fonctionne est que le rapporteur fournit un rappel, qui Angular appelle une fois tous les événements résolus, ce qui nécessite executeAsyncScript. Code ici

Dans get, le rapporteur doit interroger la page jusqu'à ce que le window.angular Global soit défini par Angular. Une façon de le faire est driver.wait(function() {driver.executeScript('return window.angular')}, 5000), mais de cette façon, le rapporteur frapperait le navigateur toutes les quelques ms. Au lieu de cela, nous faisons cela (simplifié):

functions.testForAngular = function(attempts, callback) {
  var check = function(n) {
    if (window.angular) {
      callback('good');
    } else if (n < 1) {
      callback('timedout');
    } else {
      setTimeout(function() {check(n - 1);}, 1000);
    }
  };
  check(attempts);
};

Encore une fois, cela nécessite executeAsyncScript car nous n'avons pas de valeur de retour immédiatement. Code ici


Dans l'ensemble, utilisez executeAsyncScript lorsque vous vous souciez d'une valeur de retour dans un script appelant, mais cette valeur de retour ne sera pas disponible immédiatement. Cela est particulièrement nécessaire si vous ne pouvez pas interroger le résultat, mais devez obtenir le résultat à l'aide d'un rappel ou d'une promesse (que vous devez traduire vous-même par rappel).

16
hankduan

Quand dois-je utiliser execute_async_script() au lieu de la fonction régulière execute_script()?

Lorsqu'il s'agit de vérifier les conditions côté navigateur, toutes les vérifications que vous pouvez effectuer avec execute_async_script peut être exécuté avec execute_script. Même si ce que vous vérifiez est asynchrone. Je sais parce qu'il était une fois un bogue avec execute_async_script qui a fait échouer mes tests si le script a renvoyé des résultats trop rapidement. Pour autant que je sache, le bogue a disparu maintenant, donc j'utilise execute_async_script mais depuis des mois, j'ai utilisé execute_script pour les tâches où execute_async_script aurait été plus naturel. Par exemple, effectuer une vérification qui nécessite le chargement d'un module avec RequireJS pour effectuer la vérification:

driver.execute_script("""
// Reset in case it's been used already.
window.__Selenium_test_check = undefined;
require(["foo"], function (foo) {
    window.__Selenium_test_check = foo.computeSomething();
});
""")

result = driver.wait(lambda driver: 
    driver.execute_script("return window.__Selenium_test_check;"))

L'appel require est asynchrone. Le problème avec cela, en plus de la fuite d'une variable dans l'espace global, est qu'il multiplie les demandes du réseau. Chaque execute_script l'appel est une demande de réseau. La méthode wait fonctionne par interrogation: elle exécute le test jusqu'à ce que la valeur renvoyée soit vraie. Cela signifie une requête réseau par vérification effectuée par wait (dans le code ci-dessus).

Lorsque vous testez localement, ce n'est pas grave. Si vous devez passer par le réseau parce que les navigateurs sont fournis par un service comme Sauce Labs (que j'utilise, je parle donc d'expérience), chaque demande de réseau ralentit votre suite de tests. Donc, en utilisant execute_async_script permet non seulement d'écrire un test qui semble plus naturel (appelez un rappel, comme nous le faisons normalement avec du code asynchrone, plutôt que de fuir dans l'espace global), mais cela aide également à la performance de vos tests.

result = driver.execute_async_script("""
var done = arguments[0];
require(["foo"], function (foo) {
    done(foo.computeSomething());
});
""")

La façon dont je le vois maintenant est que si un test va se connecter au code asynchrone côté navigateur pour obtenir un résultat, j'utilise execute_async_script. S'il va faire quelque chose pour lequel il n'y a pas de méthode asynchrone disponible, j'utilise execute_script.

25
Louis