Pure Implémentation de Clojure du protocole WebDriver. Utilisez cette bibliothèque pour automatiser un navigateur, testez votre comportement frontal, simulez des actions humaines ou ce que vous voulez.
Il porte le nom d'Etaoin Shrdlu - une machine à dactylographie qui est devenue vivante après une note de mystères.
Depuis 0.4.0
, l'instance du pilote est une carte mais pas un atome comme il l'était auparavant. C'était une solution difficile à décider, mais nous nous sommes débarrassés de l'atome pour suivre Clojure Way dans notre code. D'une manière générale, vous ne déregez jamais un chauffeur ou ne stockez pas quelque chose à l'intérieur. Toutes les fonctions internes utilisées pour modifier l'instance renvoient désormais une nouvelle version d'une carte. Si vous avez swap!
Ou quelque chose de similaire dans votre code pour le pilote, veuillez refacter votre code avant de mettre à jour.
Depuis 0.4.0
, la bibliothèque prend en charge les actions WebDriver. Les actions sont des commandes envoyées au pilote par lot. Voir la section liée détaillée dans le TOC.
Depuis 0.4.0
, Etaoin peut lire des fichiers de script créés dans l'IDE interactive en sélénium. Voir la section connexe ci-dessous.
Ct t
comme d'habitude.Vous êtes invités à soumettre votre entreprise dans cette liste.
Il y a deux étapes vers l'installation:
etaoin
dans votre code Clojure Ajoutez ce qui suit dans :dependencies
dans votre fichier project.clj
:
[etaoin "0.4.6"]
Fonctionne avec Clojure 1.9 et plus.
Cette page fournit des instructions sur la façon d'installer les pilotes dont vous avez besoin pour automatiser votre navigateur.
Installez les navigateurs Chrome et Firefox les téléchargeant à partir des sites officiels. Il n'y aura pas de problème sur toutes les plateformes.
Installez des pilotes spécifiques dont vous avez besoin:
Google Chrome Driver:
brew cask install chromedriver
pour les utilisateurs de Mac2.28
. 2.27
et ci-dessous a un bogue lié à la maximisation d'une fenêtre (voir [[dépannage]]).Geckodriver, un conducteur de Firefox:
brew install geckodriver
pour les utilisateurs de MacBROCKER PHantom.js:
brew install phantomjs
pour les utilisateurs de MacConducteur de safari (pour Mac uniquement):
Maintenant, vérifiez votre installation en lançant l'une de ces commandes. Pour chaque commande, un processus sans fin avec un serveur HTTP local doit démarrer.
chromedriver
geckodriver
phantomjs --wd
safaridriver -p 0
Vous pouvez exécuter des tests pour le lancement de cette bibliothèque:
lein test
Vous verrez les fenêtres du navigateur s'ouvrir et fermer en série. Les tests utilisent un fichier HTML local avec une disposition spéciale pour valider la plupart des cas.
Voir ci-dessous pour la section de dépannage si vous avez des problèmes
La bonne nouvelle que vous pouvez automatiser votre navigateur directement à partir du REP:
( use 'etaoin.api)
( require '[etaoin.keys :as k])
( def driver ( firefox )) ; ; here, a Firefox window should appear
; ; let's perform a quick Wiki session
( go driver " https://en.wikipedia.org/ " )
( wait-visible driver [{ :id :simpleSearch } { :tag :input :name :search }])
; ; search for something
( fill driver { :tag :input :name :search } " Clojure programming language " )
( fill driver { :tag :input :name :search } k/enter)
( wait-visible driver { :class :mw-search-results })
; ; select an `option` in select-box by visible text
; ; <select id="country">
; ; <option value="rf">Russia</option>
; ; <option value="usa">United States</option>
; ; <option value="uk">United Kingdom</option>
; ; <option value="fr">France</option>
; ;</select>
( select driver :country " France " )
( get-element-value driver :country )
; ;=> "fr"
; ; I'm sure the first link is what I was looking for
( click driver [{ :class :mw-search-results } { :class :mw-search-result-heading } { :tag :a }])
( wait-visible driver { :id :firstHeading })
; ; let's ensure
( get-url driver) ; ; "https://en.wikipedia.org/wiki/Clojure"
( get-title driver) ; ; "Clojure - Wikipedia"
( has-text? driver " Clojure " ) ; ; true
; ; navigate on history
( back driver)
( forward driver)
( refresh driver)
( get-title driver) ; ; "Clojure - Wikipedia"
; ; stops Firefox and HTTP server
( quit driver)
Vous voyez, toute fonction nécessite une instance de pilote comme premier argument. Vous pouvez donc le simplifier à l'aide de macros doto
:
( def driver ( firefox ))
( doto driver
( go " https://en.wikipedia.org/ " )
( wait-visible [{ :id :simpleSearch } { :tag :input :name :search }])
; ; ...
( fill { :tag :input :name :search } k/enter)
( wait-visible { :class :mw-search-results })
( click :some-button )
; ; ...
( wait-visible { :id :firstHeading })
; ; ...
( quit ))
Dans ce cas, votre code ressemble à un DSL conçu juste à de telles fins.
Vous pouvez utiliser fill-multi
pour raccourcir le code comme:
( fill driver :login " login " )
( fill driver :password " pass " )
( fill driver :textarea " some text " )
dans
( fill-multi driver { :login " login "
:password " pass "
:textarea " some text " })
Si une exception se produit lors d'une session de navigateur, le processus externe peut suspendre à jamais jusqu'à ce que vous le tue manuellement. Pour l'empêcher, utilisez with-<browser>
les macros comme suit:
( with-firefox {} ff ; ; additional options first, then bind name
( doto ff
( go " https://google.com " )
...))
Quoi qu'il arrive lors d'une session, le processus sera de toute façon arrêté.
La plupart des fonctions comme click
, fill
, etc. nécessitent un terme de requête pour découvrir un élément sur une page. Par exemple:
( click driver { :tag :button })
( fill driver { :id " searchInput " } " Clojure " )
La bibliothèque prend en charge les types et valeurs de requête suivants.
:active
représente l'élément actif actuel. Lors de l'ouverture de Google Page par exemple, il concentre le curseur sur l'entrée de recherche principale. Il n'est donc pas nécessaire de cliquer sur manuellement. Exemple:
( fill driver :active " Let's search for something " keys/enter)
Tout autre mot-clé qui indique l'ID d'un élément. Pour Google Page, c'est :lst-ib
ou "lst-ib"
(les chaînes sont également prises en charge). Le registre compte. Exemple:
( fill driver :lst-ib " What is the Matrix? " keys/enter)
une chaîne avec une expression xpath. Soyez prudent lorsque vous les écrivez manuellement. Vérifiez la section Troubleshooting
ci-dessous. Exemple:
( fill driver " .//input[@id='lst-ib'][@name='q'] " " XPath in action! " keys/enter)
Une carte avec :xpath
ou :css
avec une expression de chaîne de la syntaxe correspondante. Exemple:
( fill driver { :xpath " .//input[@id='lst-ib'] " } " XPath selector " keys/enter)
( fill driver { :css " input#lst-ib[name='q'] " } " CSS selector " keys/enter)
Voir le manuel du sélecteur CSS pour plus d'informations.
Une requête peut être n'importe quelle autre carte qui représente une expression XPATH comme données. Les règles sont:
:tag
représente le nom d'un balise. Il devient *
lorsqu'il n'est pas passé.:index
se développe dans la clause de fuite [x]
. Utile lorsque vous devez sélectionner une troisième ligne dans une table par exemple.:fn/
espace de noms et se développe en quelque chose de spécifique.Exemples:
Trouvez la première balise div
( query driver { :tag :div })
; ; expands into .//div
Trouvez la n-tth div
balise
( query driver { :tag :div :index 1 })
; ; expands into .//div[1]
Trouver la balise a
avec l'attribut de classe est égal à active
( query driver { :tag :a :class " active " })
; ; ".//a[@class="active"]"
Trouvez une forme par ses attributs:
( query driver { :tag :form :method :GET :class :message })
; ; expands into .//form[@method="GET"][@class="message"]
Trouvez un bouton par son texte (correspondance exacte):
( query driver { :tag :button :fn/text " Press Me " })
; ; .//button[text()="Press Me"]
Trouvez un nème élément ( p
, a
, n'importe quoi) avec le texte "Télécharger":
( query driver { :fn/has-text " download " :index 3 })
; ; .//*[contains(text(), "download")][3]
Trouvez un élément qui a la classe suivante:
( query driver { :tag :div :fn/has-class " overlay " })
; ; .//div[contains(@class, "overlay")]
Trouvez un élément qui a le domaine suivant dans un HREF:
( query driver { :tag :a :fn/link " google.com " })
; ; .//a[contains(@href, "google.com")]
Trouvez un élément qui a les classes suivantes à la fois:
( query driver { :fn/has-classes [ :active :sticky :marked ]})
; ; .//*[contains(@class, "active")][contains(@class, "sticky")][contains(@class, "marked")]
Trouvez les widgets d'entrée activés / désactivés:
; ; first input
( query driver { :tag :input :fn/disabled true })
; ; .//input[@disabled=true()]
( query driver { :tag :input :fn/enabled true })
; ; .//input[@enabled=true()]
; ; all inputs
( query-all driver { :tag :input :fn/disabled true })
; ; .//input[@disabled=true()]
Une requête peut être un vecteur qui se compose de toute expression mentionnée ci-dessus. Dans une telle requête, chaque terme suivant recherche un précédent récursivement.
Un exemple simple:
( click driver [{ :tag :html } { :tag :body } { :tag :a }])
Vous pouvez également combiner les expressions XPath et CSS (faites attention à un point leader dans l'expression de XPath:
( click driver [{ :tag :html } { :css " div.class " } " .//a[@class='download'] " ])
Parfois, vous devrez peut-être interagir avec le nième élément d'une requête, par exemple lorsque vous souhaitez cliquer sur le deuxième lien de cet exemple:
< ul >
< li class =" search-result " >
< a href =" a " > a </ a >
</ li >
< li class =" search-result " >
< a href =" b " > b </ a >
</ li >
< li class =" search-result " >
< a href =" c " > c </ a >
</ li >
</ ul >
Dans ce cas, vous pouvez soit utiliser la directive :index
qui est prise en charge pour les expressions XPath comme ceci:
( click driver [{ :tag :li :class :search-result :index 2 } { :tag :a }])
Ou vous pouvez utiliser le nième truc de l'enfant avec l'expression CSS comme ceci:
( click driver { :css " li.search-result:nth-child(2) a " })
Enfin, il est également possible d'obtenir le nième élément directement en utilisant query-all
:
( click-el driver ( nth ( query-all driver { :css " li.search-result a " }) 2 ))
Remarquez l'utilisation de click-el
ici, car query-all
renvoie un élément, pas un sélecteur qui peut être passé pour click
directement.
query-tree
prend des sélecteurs et agit comme un arbre. Chaque sélecteur suivant interroge les éléments des précédents. Le sélecteur de poing s'appuie sur des éléments de recherche, et les autres utilisent des éléments de fin
( query-tree driver { :tag :div } { :tag :a })
moyens
{:tag :div} -> [div1 div2 div3]
div1 -> [a1 a2 a3]
div2 -> [a4 a5 a6]
div3 -> [a7 a8 a9]
Le résultat sera donc [A1 ... A9]
Pour interagir avec les éléments trouvés via une requête, vous devez passer le résultat de la requête à click-el
ou fill-el
:
( click-el driver ( first ( query-all driver { :tag :a })))
Vous pouvez donc collecter des éléments dans un vecteur et interagir arbitrairement avec eux à tout moment:
( def elements ( query-all driver { :tag :input :type :text })
( fill-el driver ( first elements) " This is a test " )
( fill-el driver ( rand-nth elements) " I like tests! " )
Dans le but d'émuler l'entrée humaine, vous pouvez utiliser la fonction fill-human
. Les options suivantes sont activées par défaut:
{ :mistake-prob 0.1 ; ; a real number from 0.1 to 0.9, the higher the number, the more typos will be made
:pause-max 0.2 } ; ; max typing delay in seconds
Et vous pouvez les redéfinir:
( fill-human driver q text { :mistake-prob 0.5
:pause-max 1 })
; ; or just use default opts by omitting them
( fill-human driver q text)
Pour une entrée multiple avec une émulation humaine, utilisez fill-human-multi
( fill-human-multi driver { :login " login "
:pass " password "
:textarea " some text " }
{ :mistake-prob 0.5
:pause-max 1 })
La fonction click
déclenche la souris gauche cliquez sur un élément trouvé par un terme de requête:
( click driver { :tag :button })
La fonction click
utilise uniquement le premier élément trouvé par la requête, ce qui conduit parfois à cliquer sur les mauvais éléments. Pour vous assurer qu'il existe un et un seul élément trouvé, utilisez la fonction click-single
. Il agit de la même manière mais soulève une exception lorsque l'interrogation de la page renvoie plusieurs éléments:
( click-single driver { :tag :button :name " search " })
Un double clic est utilisé rarement sur le Web mais est possible avec la fonction double-click
(Chrome, Phantom.js):
( double-click driver { :tag :dbl-click-btn })
Il y a aussi un tas de fonctions de clic "aveugles". Ils déclenchent des clics de souris sur la position actuelle de la souris:
( left-click driver)
( middle-click driver)
( right-click driver)
Un autre tas de fonctions font de même mais déplacez le pointeur de la souris vers un élément spécifié avant de cliquer dessus:
( left-click-on driver { :tag :img })
( middle-click-on driver { :tag :img })
( right-click-on driver { :tag :img })
Un clic de souris intermédiaire est utile lors de l'ouverture d'un lien dans un nouvel onglet d'arrière-plan. Le clic droit est parfois utilisé pour imiter un menu contextuel dans les applications Web.
La bibliothèque prend en charge les actions WebDriver. En général, les actions sont des commandes décrivant des dispositifs d'entrée virtuels.
{ :actions [{ :type " key "
:id " some name "
:actions [{ :type " keyDown " :value cmd}
{ :type " keyDown " :value " a " }
{ :type " keyUp " :value " a " }
{ :type " keyUp " :value cmd}
{ :type " pause " :duration 100 }]}
{ :type " pointer "
:id " UUID or some name "
:parameters { :pointerType " mouse " }
:actions [{ :type " pointerMove " :origin " pointer " :x 396 :y 323 }
; ; double click
{ :type " pointerDown " :duration 0 :button 0 }
{ :type " pointerUp " :duration 0 :button 0 }
{ :type " pointerDown " :duration 0 :button 0 }
{ :type " pointerUp " :duration 0 :button 0 }]}]}
Vous pouvez créer une carte manuellement et l'envoyer à la méthode perform-actions
:
( def keyboard-input { :type " key "
:id " some name "
:actions [{ :type " keyDown " :value cmd}
{ :type " keyDown " :value " a " }
{ :type " keyUp " :value " a " }
{ :type " keyUp " :value cmd}
{ :type " pause " :duration 100 }]})
( perform-actions driver keyboard-input)
ou utilisez des emballages. Vous devez d'abord créer des périphériques d'entrée virtuels, par exemple:
( def keyboard ( make-key-input ))
puis le remplir avec les actions nécessaires:
( -> keyboard
( add-key-down keys/shift-left)
( add-key-down " a " )
( add-key-up " a " )
( add-key-up keys/shift-left))
Exemple étendu:
( let [driver ( chrome )
_ ( go driver " https://google.com " )
search-box ( query driver { :name :q })
mouse ( -> ( make-mouse-input )
( add-pointer-click-el search-box))
keyboard ( -> ( make-key-input )
add-pause
( with-key-down keys/shift-left
( add-key-press " e " ))
( add-key-press " t " )
( add-key-press " a " )
( add-key-press " o " )
( add-key-press " i " )
( add-key-press " n " )
( add-key-press keys/enter))]
( perform-actions driver keyboard mouse)
( quit driver))
Pour effacer l'état des périphériques d'entrée virtuels, relâchez toutes les touches pressées, etc., utilisez la méthode release-actions
:
( release-actions driver)
Cliquez sur un bouton d'entrée de fichier ouvre une boîte de dialogue spécifique au système d'exploitation avec lequel vous n'êtes pas autorisé à interagir à l'aide du protocole WebDriver. Utilisez la fonction upload-file
pour joindre un fichier local dans un widget d'entrée de fichier. La fonction prend un sélecteur qui pointe vers une entrée de fichier et soit un chemin complet en tant que chaîne ou une instance native java.io.File
. Le fichier doit exister ou vous obtiendrez une exception autrement. Exemple d'utilisation:
( def driver ( chrome ))
; ; open a web page that serves uploaded files
( go driver " http://nervgh.github.io/pages/angular-file-upload/examples/simple/ " )
; ; bound selector to variable; you may also specify an id, class, etc
( def input { :tag :input :type :file })
; ; upload an image with the first one file input
( def my-file " /Users/ivan/Downloads/sample.png " )
( upload-file driver input my-file)
; ; or pass a native Java object:
( require '[clojure.java.io :as io])
( def my-file ( io/file " /Users/ivan/Downloads/sample.png " ))
( upload-file driver input my-file)
L'appel d'une fonction screenshot
vide la page actuelle dans une image PNG sur votre disque:
( screenshot driver " page.png " ) ; ; relative path
( screenshot driver " /Users/ivan/page.png " ) ; ; absolute path
Un objet de fichier Java natif est également pris en charge:
; ; when imported as `[clojure.java.io :as io]`
( screenshot driver ( io/file " test.png " ))
; ; native object
( screenshot driver ( java.io.File. " test-native.png " ))
Avec Firefox et Chrome, vous pouvez ne pas capturer la page entière mais un seul élément, dites un div, un widget d'entrée ou autre chose. Cela ne fonctionne pas avec d'autres navigateurs pour l'instant. Exemple:
( screenshot-element driver { :tag :div :class :smart-widget } " smart_widget.png " )
Avec la macro with-screenshots
, vous pouvez faire une capture d'écran après chaque formulaire
( with-screenshots driver " ../screenshots "
( fill driver :simple-input " 1 " )
( fill driver :simple-input " 2 " )
( fill driver :simple-input " 3 " ))
Ce qui équivaut à un record:
( fill driver :simple-input " 1 " )
( screenshot driver " ../screenshots/chrome-...123.png " )
( fill driver :simple-input " 2 " )
( screenshot driver " ../screenshots/chrome-...124.png " )
( fill driver :simple-input " 3 " )
( screenshot driver " ../screenshots/chrome-...125.png " )
Récemment, Google Chrome et plus tard Firefox ont commencé à prendre en charge une fonctionnalité nommée Mode sans tête. Lorsqu'il est sans tête, aucune des fenêtres d'interface utilisateur ne se produit sur l'écran, seule la sortie STDOUT entre dans la console. Cette fonctionnalité vous permet d'exécuter des tests d'intégration sur des serveurs qui n'ont pas de périphérique de sortie graphique.
Assurez-vous que votre navigateur prend en charge le mode sans tête en vérifiant si elle accepte --headles
argument de ligne de commande lors de l'exécution du terminal. Le conducteur Phantom.js est sans tête par sa nature (il n'a jamais été développé pour rendre l'interface utilisateur).
Lorsque vous démarrez un pilote, passez :headless
pour passer en mode sans tête. Remarque, seule la dernière version de Chrome et Firefox est prise en charge. Pour les autres conducteurs, le drapeau sera ignoré.
( def driver ( chrome { :headless true })) ; ; runs headless Chrome
ou
( def driver ( firefox { :headless true })) ; ; runs headless Firefox
Pour vérifier qu'un pilote a été exécuté en mode sans tête, utilisez headless?
prédicat:
( headless? driver) ; ; true
Remarque, il reviendra toujours vrai pour les instances Phantom.js.
Il existe plusieurs raccourcis pour exécuter Chrome ou Firefox en mode sans tête par défaut:
( def driver ( chrome-headless ))
; ; or
( def driver ( firefox-headless {...})) ; ; with extra settings
; ; or
( with-chrome-headless nil driver
( go driver " http://example.com " ))
( with-firefox-headless {...} driver ; ; extra settings
( go driver " http://example.com " ))
Il y a aussi des macroses when-headless
et when-not-headless
qui permettent d'effectuer un tas de commandes uniquement si un navigateur est en mode sans tête ou non respectivement:
( with-chrome nil driver
...
( when-not-headless driver
... some actions that might be not available in headless mode)
... common actions for both versions)
Pour vous connecter à un pilote déjà en cours d'exécution sur un hôte local ou distant, vous devez spécifier le paramètre :host
qui pourrait être soit un nom d'hôte (localhost, some.remote.host.net) ou une adresse IP (127.0.0.1, 183.102.156.31 ) et le :port
. Si le port n'est pas spécifié, le port par défaut est défini.
Exemple:
; ; Chrome
( def driver ( chrome { :host " 127.0.0.1 " :port 9515 })) ; ; for connection to driver on localhost on port 9515
; ; Firefox
( def driver ( firefox { :host " 192.168.1.11 " })) ; ; the default port for firefox is 4444
Pour travailler avec le pilote dans Docker, vous pouvez prendre des images prêtes à l'emploi:
Exemple pour Chrome:
docker run --name chromedriver -p 9515:4444 -d -e CHROMEDRIVER_WHITELISTED_IPS='' robcherry/docker-chromedriver:latest
pour Firefox:
docker run --name geckodriver -p 4444:4444 -d instrumentisto/geckodriver
Pour vous connecter au pilote, vous avez juste besoin de spécifier le paramètre :host
en tant que localhost
ou 127.0.0.1
et le :port
sur lequel il s'exécute. Si le port n'est pas spécifié, le port par défaut est défini.
( def driver ( chrome-headless { :host " localhost " :port 9515 :args [ " --no-sandbox " ]}))
( def driver ( firefox-headless { :host " localhost " })) ; ; will try to connect to port 4444
Pour définir les paramètres proxy, utilisez des variables d'environnement HTTP_PROXY
/ HTTPS_PROXY
ou passez une carte du type suivant:
{ :proxy { :http " some.proxy.com:8080 "
:ftp " some.proxy.com:8080 "
:ssl " some.proxy.com:8080 "
:socks { :host " myproxy:1080 " :version 5 }
:bypass [ " http://this.url " " http://that.url " ]
:pac-url " localhost:8888 " }}
; ; example
( chrome { :proxy { :http " some.proxy.com:8080 "
:ssl " some.proxy.com:8080 " }})
Remarque: A: Pac-URL pour un fichier d'autoconfiguration proxy. Utilisé avec Safari car les autres options de proxy ne fonctionnent pas dans ce navigateur.
Pour affiner le proxy, vous pouvez utiliser l'objet original et le transmettre aux capacités:
{ :capabilities { :proxy { :proxyType " manual "
:proxyAutoconfigUrl " some.proxy.com:8080 "
:ftpProxy " some.proxy.com:8080 "
:httpProxy " some.proxy.com:8080 "
:noProxy [ " http://this.url " " http://that.url " ]
:sslProxy " some.proxy.com:8080 "
:socksProxy " some.proxy.com:1080 "
:socksVersion 5 }}}
( chrome { :capabilities { :proxy {...}}})
Avec les mises à jour récentes, la bibliothèque apporte une excellente fonctionnalité. Vous pouvez maintenant tracer des événements qui proviennent du panneau Devtools. Cela signifie que tout ce que vous voyez dans la console du développeur est maintenant disponible via l'API. Cela ne fonctionne que avec Google Chrome maintenant.
Pour démarrer un pilote avec des paramètres de développement spéciaux spécifiés, passez simplement une carte vide au champ :dev
lors de l'exécution d'un pilote:
( def c ( chrome { :dev {}}))
La valeur ne doit pas être nil
. Lorsqu'il s'agit d'une carte vide, une fonction spéciale prend des défauts par défaut. Voici une version complète des paramètres de développement avec toutes les valeurs possibles spécifiées.
( def c ( chrome { :dev
{ :perf
{ :level :all
:network? true
:page? true
:interval 1000
:categories [ :devtools
:devtools.network
:devtools.timeline ]}}}))
Sous le capot, il remplit un dictionnaire spécial perfLoggingPrefs
à l'intérieur de l'objet chromeOptions
.
Maintenant que votre navigateur accumule ces événements, vous pouvez les lire à l'aide d'un espace de noms dev
spécial.
( go c " http://google.com " )
; ; wait until the page gets loaded
; ; load the namespace
( require '[etaoin.dev :as dev])
Ayons une liste de toutes les demandes HTTP se produisent pendant la page.
( def reqs ( dev/get-requests c))
; ; reqs is a vector of maps
( count reqs)
; ; 19
; ; what were their types?
( set ( map :type reqs))
; ; #{:script :other :document :image :xhr}
; ; we've got Js requests, images, AJAX and other stuff
; ; check the last one request, it's an image named tia.png
( -> reqs last clojure.pprint/pprint)
{ :state 4 ,
:id " 1000052292.8 " ,
:type :image ,
:xhr? false ,
:url " https://www.gstatic.com/inputtools/images/tia.png " ,
:with-data? nil,
:request
{ :method :get ,
:headers
{ :Referer " https://www.google.com/ " ,
:User-Agent
" Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36 " }},
:response
{ :status 200 ,
:headers {}, ; ; truncated
:mime " image/png " ,
:remote-ip " 173.194.73.94 " },
:done? true }
Étant donné que nous sommes principalement intéressés par les demandes AJAX, il existe une fonction get-ajax
qui fait de même mais filtre les demandes XHR:
( -> c dev/get-ajax last clojure.pprint/pprint)
{ :state 4 ,
:id " 1000051989.41 " ,
:type :xhr ,
:xhr? true ,
:url
" https://www.google.com/complete/search?q=clojure%20spec&cp=12&client=psy-ab&xssi=t&gs_ri=gws-wiz&hl=ru&authuser=0&psi=4iUbXdapJsbmrgTVt7H4BA.1562060259137&ei=4iUbXdapJsbmrgTVt7H4BA " ,
:with-data? nil,
:request
{ :method :get ,
:headers
{ :Referer " https://www.google.com/ " ,
:User-Agent
" Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36 " }},
:response
{ :status 200 ,
:headers {}, ; ; truncated
:mime " application/json " ,
:remote-ip " 74.125.131.99 " },
:done? true }
Un modèle typique d'utilisation de get-ajax
est le suivant. Vous souhaitez vérifier si une certaine demande a été licenciée sur le serveur. Vous appuyez donc sur un bouton, attendez un certain temps, puis lisez les demandes faites par votre navigateur.
Ayant une liste de demandes, vous recherchez celle dont vous avez besoin (par exemple par son URL), puis vérifiez son état. Le champ :state
a la même sémantique que le XMLHttpRequest.readyState
l'a fait. C'est un entier de 1 à 4 avec le même comportement.
Pour vérifier si une demande a été terminée, fait ou a échoué, utilisez ces prédicats:
( def req ( last reqs))
( dev/request-done? req)
; ; true
( dev/request-failed? req)
; ; false
( dev/request-success? req)
; ; true
Notez cette request-done?
ne signifie pas que la demande a réussi. Cela signifie seulement que son pipeline a atteint une dernière étape.
AVERTISSEMENT: Lorsque vous lisez les journaux de développement, vous les consommez à partir d'un tampon interne qui est rincé. Le deuxième appel à get-requests
ou get-ajax
renverra une liste vide.
Vous souhaitez peut-être collecter ces journaux par vous-même. Une fonction dev/get-performance-logs
renvoie une liste de journaux afin que vous les accumulez dans un atome ou autre:
( def logs ( atom []))
; ; repeat that form from time to time
( do ( swap! logs concat ( dev/get-performance-logs c))
true )
( count @logs)
; ; 76
Il y a logs->requests
et logs->ajax
qui convertissent les journaux en demandes. Contrairement aux get-requests
et get-ajax
, ce sont des fonctions pures et ne rincera rien.
( dev/logs->requests @logs)
Lorsque vous travaillez avec des journaux et des demandes, faites attention à leur décompte et à leur taille. Les cartes ont beaucoup de clés et la quantité d'articles dans les collections pourrait être énorme. L'impression d'un tas d'événements pourrait geler votre éditeur. Envisagez d'utiliser la fonction clojure.pprint/pprint
car elle s'appuie sur le niveau maximum et les limites de longueur.
Parfois, il peut être difficile de découvrir ce qui n'a pas fonctionné lors de la dernière session des tests d'interface utilisateur. Une macro spéciale with-postmortem
enregistre certaines données utiles sur le disque avant que l'exception ne soit déclenchée. Ces données sont une capture d'écran, du code HTML et des journaux de console JS. Remarque: tous les navigateurs ne prennent pas en charge les journaux JS.
Exemple:
( def driver ( chrome ))
( with-postmortem driver { :dir " /Users/ivan/artifacts " }
( click driver :non-existing-element ))
Une exception augmentera, mais dans /Users/ivan/artifacts
il y aura trois fichiers nommés par un modèle <browser>-<host>-<port>-<datetime>.<ext>
:
firefox-127.0.0.1-4444-2017-03-26-02-45-07.png
: une capture d'écran réelle de la page du navigateur;firefox-127.0.0.1-4444-2017-03-26-02-45-07.html
: le contenu HTML du navigateur actuel;firefox-127.0.0.1-4444-2017-03-26-02-45-07.json
: un fichier JSON avec des journaux de console; Ce sont un vecteur d'objets.Le gestionnaire prend une carte des options avec les clés suivantes. Tous pourraient être absents.
{ ; ; default directory where to store artifacts
; ; might not exist, will be created otherwise. pwd is used when not passed
:dir " /home/ivan/UI-tests "
; ; a directory where to store screenshots; :dir is used when not passed
:dir-img " /home/ivan/UI-tests/screenshots "
; ; the same but for HTML sources
:dir-src " /home/ivan/UI-tests/HTML "
; ; the same but for console logs
:dir-log " /home/ivan/UI-tests/console "
; ; a string template to format a date; See SimpleDateFormat Java class
:date-format " yyyy-MM-dd-HH-mm-ss " }
Fonction (get-logs driver)
Renvoie les journaux du navigateur en tant que vecteur de cartes. Chaque carte a la structure suivante:
{ :level :warning ,
:message " 1,2,3,4 anonymous (:1) " ,
:timestamp 1511449388366 ,
:source nil,
:datetime #inst " 2017-11-23T15:03:08.366-00:00 " }
Actuellement, les journaux sont disponibles dans Chrome et Phantom.js uniquement. Veuillez noter que le texte du message et le type de source dépendent fortement du navigateur. Chrome essuie les journaux une fois qu'ils ont été lus. Phantom.js les garde mais seulement jusqu'à ce que vous modifiiez la page.
Lors de l'exécution d'une instance de pilote, une carte des paramètres supplémentaires peut être transmise pour modifier le comportement du navigateur:
( def driver ( chrome { :path " /path/to/driver/binary " }))
Ci-dessous, voici une carte des paramètres du support de la bibliothèque. Tous peuvent être ignorés ou nuls. Certains d'entre eux, s'ils ne sont pas passés, sont tirés de la carte defaults
.
{ ; ; Host and port for webdriver's process. Both are taken from defaults
; ; when are not passed. If you pass a port that has been already taken,
; ; the library will try to take a random one instead.
:host " 127.0.0.1 "
:port 9999
; ; Path to webdriver's binary file. Taken from defaults when not passed.
:path-driver " /Users/ivan/Downloads/geckodriver "
; ; Path to the driver's binary file. When not passed, the driver discovers it
; ; by its own.
:path-browser " /Users/ivan/Downloads/firefox/firefox "
; ; Extra command line arguments sent to the browser's process. See your browser's
; ; supported flags.
:args [ " --incognito " " --app " " http://example.com " ]
; ; Extra command line arguments sent to the webdriver's process.
:args-driver [ " -b " " /path/to/firefox/binary " ]
; ; Sets browser's minimal logging level. Only messages with level above
; ; that one will be collected. Useful for fetching Javascript logs. Possible
; ; values are: nil (aliases :off, :none), :debug, :info, :warn (alias :warning),
; ; :err (aliases :error, :severe, :crit, :critical), :all. When not passed,
; ; :all is set.
:log-level :err ; ; to show only errors but not debug
; ; Sets driver's log level.
; ; The value is a string. Possible values are:
; ; chrome: [ALL, DEBUG, INFO, WARNING, SEVERE, OFF]
; ; phantomjs: [ERROR, WARN, INFO, DEBUG] (default INFO)
; ; firefox [fatal, error, warn, info, config, debug, trace]
:driver-log-level
; ; Paths to the driver's log files as strings.
; ; When not set, the output goes to /dev/null (or NUL on Windows)
:log-stdout
:log-stderr
; ; Path to a custorm browser profile. See the section below.
:profile " /Users/ivan/Library/Application Support/Firefox/Profiles/iy4iitbg.Test "
; ; Env variables sent to the driver's process.
:env { :MOZ_CRASHREPORTER_URL " http://test.com " }
; ; Initial window size.
:size [ 1024 680 ]
; ; Default URL to open. Works only in FF for now.
:url " http://example.com "
; ; Override the default User-Agent. Useful for headless mode.
:user-agent " Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) "
; ; Where to download files.
:download-dir " /Users/ivan/Desktop "
; ; Driver-specific options. Make sure you have read the docs before setting them.
:capabilities { :chromeOptions { :args [ " --headless " ]}}}
Lorsque vous accédez à une certaine page, le conducteur attend que la page entière ait été complètement chargée. Ce qui va bien dans la plupart des cas ne reflète pas la façon dont les êtres humains interagissent avec Internet.
Modifiez ce comportement par défaut avec l'option :load-strategy
. Il y a trois valeurs possibles pour cela :: :none
, :eager
et :normal
qui est la valeur par défaut lorsqu'elle n'est pas passé.
Lorsque vous passez :none
, le pilote répond immédiatement, vous êtes donc invité à exécuter les instructions suivantes. Par exemple:
( def c ( chrome ))
( go c " http://some.slow.site.com " )
; ; you'll hang on this line until the page loads
( do-something )
Maintenant, lors de la réussite de l'option de stratégie de charge:
( def c ( chrome { :load-strategy :none }))
( go c " http://some.slow.site.com " )
; ; no pause, acts immediately
( do-something )
Pour l'option :eager
, cela ne fonctionne qu'avec Firefox au moment d'ajouter la fonctionnalité à la bibliothèque.
Il existe une option pour saisir simultanément une série de clés. Cela est utile pour imiter la maintenance d'une clé système comme le contrôle, le changement ou quoi que ce soit lors de la saisie.
L'espace de noms etaoin.keys
comporte un tas de constantes clés ainsi qu'un ensemble de fonctions liées à l'entrée.
( require '[etaoin.keys :as keys])
Un exemple rapide de la saisie des personnages ordinaires tenant le quart de travail:
( def c ( chrome ))
( go c " http://google.com " )
( fill-active c ( keys/with-shift " caps is great " ))
L'entrée principale est remplie de "Caps est super". Maintenant, vous souhaitez supprimer le dernier mot. Dans Chrome, cela se fait en appuyant sur BackSpace Holding Alt. Faisons ça:
( fill-active c ( keys/with-alt keys/backspace))
Maintenant, vous n'avez que "Caps est" dans l'entrée.
Considérez un exemple plus complexe qui répète le comportement des utilisateurs réels. Vous souhaitez tout supprimer de l'entrée. Tout d'abord, vous déplacez le garet au tout début. Ensuite, déplacez-le à la fin de maintien de maintien pour que tout soit sélectionné. Enfin, vous appuyez sur Supprimer pour effacer le texte sélectionné.
Le combo est:
( fill-active c keys/home ( keys/with-shift keys/end) keys/delete)
Il existe également des fonctions with-ctrl
et with-command
qui agissent de la même manière.
Faites attention, ces fonctions ne s'appliquent pas aux raccourcis du navigateur mondial. Par exemple, ni "Command + R" ni "Command + T" recharger la page ni ouvrir un nouvel onglet.
Toutes les keys/with-*
ne sont que des emballages sur la fonction keys/chord
qui pourraient être utilisés pour des cas complexes.
Pour spécifier votre propre répertoire où télécharger des fichiers, passez :download-dir
dans une carte d'option lors de l'exécution d'un pilote:
( def driver ( chrome { :download-dir " /Users/ivan/Desktop " }))
Maintenant, une fois que vous avez cliqué sur un lien, un fichier doit être placé dans ce dossier. Actuellement, seuls Chrome et Firefox sont pris en charge.
Firefox a besoin de spécifier des types mime de ces fichiers qui doivent être téléchargés sans afficher une boîte de dialogue système. Par défaut, lorsque le paramètre :download-dir
est passé, la bibliothèque ajoute les types de mime les plus courants: archives, fichiers multimédias, documents de bureau, etc. Si vous devez en ajouter votre propre, remplacez manuellement cette préférence:
( def driver ( firefox { :download-dir " /Users/ivan/Desktop "
:prefs { :browser.helperApps.neverAsk.saveToDisk
" some-mime/type-1;other-mime/type-2 " }}))
Pour vérifier si un fichier a été téléchargé lors des tests d'interface utilisateur, consultez la section de test ci-dessous.
Définissez un en-tête d'agent utilisateur personnalisé avec l'option :user-agent
lors de la création d'un pilote, par exemple:
( def f ( firefox { :user-agent " Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) " }))
Pour obtenir la valeur actuelle de l'en-tête dans l'exécution, utilisez la fonction:
( get-user-agent f)
; ; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
La définition de cet en-tête est assez importante pour les navigateurs sans tête car la plupart des sites vérifient si l'agent utilisateur comprend la chaîne "sans tête". Cela pourrait entraîner une réponse 403 ou un comportement étrange du site.
Lorsque vous exécutez Chrome ou Firefox, vous pouvez spécifier un profil spécial conçu à des fins de test. Un profil est un dossier qui conserve les paramètres du navigateur, l'historique, les signets et autres données spécifiques à l'utilisateur.
Imaginez que vous souhaitez exécuter vos tests d'intégration contre un utilisateur qui a désactivé l'exécution JavaScript ou le rendu d'image. Préparer un profil spécial pour cette tâche serait un bon choix.
chrome://version/
Page. Copiez le chemin de fichier qui est sous la légende Profile Path
.-P
, -p
ou -ProfileManager
Key comme le décrit la page officielle.about:support
. Près de la légende Profile Folder
, appuyez sur le bouton Show in Finder
. Une nouvelle fenêtre de dossier doit apparaître. Copiez son chemin à partir de là. Une fois que vous avez un chemin de profil, lancez un pilote avec une clé :profile
spéciale comme suit:
; ; Chrome
( def chrome-profile
" /Users/ivan/Library/Application Support/Google/Chrome/Profile 2/Default " )
( def chrm ( chrome { :profile chrome-profile}))
; ; Firefox
( def ff-profile
" /Users/ivan/Library/Application Support/Firefox/Profiles/iy4iitbg.Test " )
( def ff ( firefox { :profile ff-profile}))
La bibliothèque expédie un ensemble de fonctions pour faire défiler la page.
Le plus important, scroll-query
saute le premier élément trouvé avec le terme de requête:
( def driver ( chrome ))
; ; the form button placed somewhere below
( scroll-query driver :button-submit )
; ; the main article
( scroll-query driver { :tag :h1 })
Pour sauter à la position absolue, utilisez simplement scroll
comme suit:
( scroll driver 100 600 )
; ; or pass a map with x and y keys
( scroll driver { :x 100 :y 600 })
Pour faire défiler relativement, utilisez scroll-by
avec des valeurs de décalage:
; ; keeps the same horizontal position, goes up for 100 pixels
( scroll-by driver 0 -100 ) ; ; map parameter is also supported
Il y a deux raccourcis pour sauter en haut ou en bas de la page:
( scroll-bottom driver) ; ; you'll see the footer...
( scroll-top driver) ; ; ...and the header again
Les fonctions suivantes font défiler la page dans toutes les directions:
( scroll-down driver 200 ) ; ; scrolls down by 200 pixels
( scroll-down driver) ; ; scrolls down by the default (100) number of pixels
( scroll-up driver 200 ) ; ; the same, but scrolls up...
( scroll-up driver)
( scroll-left driver 200 ) ; ; ...left
( scroll-left driver)
( scroll-right driver 200 ) ; ; ... and right
( scroll-right driver)
Une note, dans tous les cas, les actions de défilement sont servies avec JavaScript. Assurez-vous que votre navigateur l'a activé.
En travaillant avec la page, vous ne pouvez pas interagir avec les éléments qui sont placés dans un cadre ou un iframe. Les fonctions ci-dessous changent le contexte actuel sur un cadre spécifique:
( switch-frame driver :frameId ) ; ; now you are inside an iframe with id="frameId"
( click driver :someButton ) ; ; click on a button inside that iframe
( switch-frame-top driver) ; ; switches on the top of the page again
Les cadres pourraient être imbriqués les uns dans les autres. Les fonctions en tiennent compte. Dites que vous avez une disposition HTML comme ceci:
< iframe src =" ... " >
< iframe src =" ... " >
< button id =" the-goal " >
</ frame >
</ frame >
Afin que vous puissiez atteindre le bouton avec le code suivant:
( switch-frame-first driver) ; ; switches to the first top-level iframe
( switch-frame-first driver) ; ; the same for an iframe inside the previous one
( click driver :the-goal )
( switch-frame-parent driver) ; ; you are in the first iframe now
( switch-frame-parent driver) ; ; you are at the top
Pour réduire le nombre de lignes de code, il existe une macro spéciale with-frame
. Il change temporaire des cadres lors de l'exécution du corps renvoyant sa dernière expression et passant par la suite à la trame précédente.
( with-frame driver { :id :first-frame }
( with-frame driver { :id :nested-frame }
( click driver { :id :nested-button })
42 ))
Le code ci-dessus renvoie 42
en restant dans la même trame qui a été avant d'évaluer les macros.
Pour évaluer un code JavaScript dans un navigateur, exécutez:
( js-execute driver " alert(1) " )
Vous pouvez transmettre tous les paramètres supplémentaires dans l'appel et les cuire dans un script avec les arguments
objet de type tableau:
( js-execute driver " alert(arguments[2].foo) " 1 false { :foo " hello! " })
En conséquence, hello!
La chaîne apparaîtra dans la boîte de dialogue.
Pour retourner toutes les données dans Clojure, ajoutez simplement return
dans votre script:
( js-execute driver " return {foo: arguments[2].foo, bar: [1, 2, 3]} "
1 false { :foo " hello! " })
; ; {:bar [1 2 3], :foo "hello!"}
Si votre script effectue des demandes AJAX ou fonctionne sur setTimeout
ou tout autre truc asynchrone, vous ne pouvez pas simplement return
le résultat. Au lieu de cela, un rappel spécial doit être appelé contre les données que vous souhaitez obtenir. Le WebDriver transmet ce rappel comme le dernier argument pour votre script et pourrait être atteint avec l'objet de type argument arguments
.
Exemple:
( js-async
driver
" var args = arguments; // preserve the global args
var callback = args[args.length-1];
setTimeout(function() {
callback(args[0].foo.bar.baz);
},
1000); "
{ :foo { :bar { :baz 42 }}})
Renvoie 42
au code Clojure.
Pour évaluer un script asynchrone, vous devez soit configurer un délai d'expiration spécial pour cela:
( set-script-timeout driver 5 ) ; ; in seconds
Ou enveloppez le code dans une macros qui le fait temporaire:
( with-script-timeout driver 30
( js-async driver " some long script " ))
La principale différence entre un programme et un être humain est que le premier fonctionne très rapidement. Cela signifie si vite, qu'un navigateur ne peut parfois pas rendre de nouveaux HTML dans le temps. Donc, après chaque action, vous feriez mieux de mettre une fonction wait-<something>
qui interdit simplement un navigateur jusqu'à ce que le prédicat évalue en vrai. Ou simplement (wait <seconds>)
Si vous ne vous souciez pas de l'optimisation.
La macro with-wait
peut être utile lorsque vous devez prétendre chaque action avec (wait n)
. Par exemple, le formulaire suivant
( with-chrome {} driver
( with-wait 3
( go driver " http://site.com " )
( click driver { :id " search_button " })))
se transforme en quelque chose comme ceci:
( with-chrome {} driver
( wait 3 )
( go driver " http://site.com " )
( wait 3 )
( click driver { :id " search_button " }))
et renvoie ainsi le résultat de la dernière forme du corps d'origine.
Il existe une autre macro (doto-wait n driver & body)
qui agit comme le doto
standard mais appliquez chaque formulaire avec (wait n)
. Par exemple:
( with-chrome {} driver
( doto-wait 1 driver
( go " http://site.com " )
( click :this-link )
( click :that-button )
...etc))
Le formulaire final serait quelque chose comme ceci:
( with-chrome {} driver
( doto driver
( wait 1 )
( go " http://site.com " )
( wait 1 )
( click :this-link )
( wait 1 )
( click :that-button )
...etc))
En plus with-wait
et do-wait
il existe un certain nombre de fonctions d'attente: wait-visible
, wait-has-alert
, wait-predicate
, etc. (voir la liste complète dans la section correspondante). Ils acceptent les valeurs de délai / intervalle par défaut qui peuvent être redéfinies à l'aide des macros de with-wait-timeout
et with-wait-interval
, respectivement.
Exemple du test d'Etaoin:
( deftest test-wait-has-text
( testing " wait for text simple "
( with-wait-timeout 15 ; ; time in seconds
( doto *driver*
( refresh )
( wait-visible { :id :document-end })
( click { :id :wait-button })
( wait-has-text :wait-span " -secret- " ))
( is true " text found " ))))
Attendez le texte:
wait-has-text
attend qu'un élément ait du texte n'importe où à l'intérieur (y compris le HTML intérieur).
( wait-has-text driver :wait-span " -secret- " )
wait-has-text-everywhere
comme wait-has-text
mais recherche du texte sur toute la page
( wait-has-text-everywhere driver " -secret- " )
Pour que votre test ne dépend pas les uns des autres, vous devez les envelopper dans un luminaire qui créera une nouvelle instance d'un pilote et l'arrêtera correctement à la fin si chaque test.
Une bonne solution pourrait être d'avoir une variable globale (non liée par défaut) qui pointera vers le pilote cible pendant les tests.
( ns project.test.integration
" A module for integration tests "
( :require [clojure.test :refer :all ]
[etaoin.api :refer :all ]))
( def ^:dynamic *driver*)
( defn fixture-driver
" Executes a test running a driver. Bounds a driver
with the global *driver* variable. "
[f]
( with-chrome {} driver
( binding [*driver* driver]
( f ))))
( use-fixtures
:each ; ; start and stop driver for each test
fixture-driver)
; ; now declare your tests
( deftest ^:integration
test-some-case
( doto *driver*
( go url-project)
( click :some-button )
( refresh )
...
))
Si pour une raison quelconque, vous souhaitez utiliser une seule instance, vous pouvez utiliser des luminaires comme ceci:
( ns project.test.integration
" A module for integration tests "
( :require [clojure.test :refer :all ]
[etaoin.api :refer :all ]))
( def ^:dynamic *driver*)
( defn fixture-browser [f]
( with-chrome-headless { :args [ " --no-sandbox " ]} driver
( disconnect-driver driver)
( binding [*driver* driver]
( f ))
( connect-driver driver)))
; ; creating a session every time that automatically erases resources
( defn fixture-clear-browser [f]
( connect-driver *driver*)
( go *driver* " http://google.com " )
( f )
( disconnect-driver *driver*))
; ; this is run `once` before running the tests
( use-fixtures
:once
fixture-browser)
; ; this is run `every` time before each test
( use-fixtures
:each
fixture-clear-browser)
...some tests
Pour des tests plus rapides, vous pouvez utiliser cet exemple:
.....
( defn fixture-browser [f]
( with-chrome-headless { :args [ " --no-sandbox " ]} driver
( binding [*driver* driver]
( f ))))
; ; note that resources, such as cookies, are deleted manually,
; ; so this does not guarantee that the tests are clean
( defn fixture-clear-browser [f]
( delete-cookies *driver*)
( go *driver* " http://google.com " )
( f ))
......
Dans l'exemple ci-dessus, nous avons examiné un cas lorsque vous effectuez des tests sur un seul type de pilote. Cependant, vous voudrez peut-être tester votre site sur plusieurs pilotes, par exemple, Chrome et Firefox. Dans ce cas, votre match peut devenir un peu plus complexe:
( def driver-type [ :firefox :chrome ])
( defn fixture-drivers [f]
( doseq [type driver-types]
( with-driver type {} driver
( binding [*driver* driver]
( testing ( format " Testing in %s browser " ( name type))
( f ))))))
Maintenant, chaque test sera exécuté deux fois dans les navigateurs Firefox et Chrome. Veuillez noter que l'appel de test est apparenté avec la macro testing
qui place le nom du pilote dans le rapport. Une fois que vous avez eu une erreur, vous trouverez facilement quel pilote a échoué les tests exactement.
Pour enregistrer certains artefacts en cas d'exception, enveloppez le corps de votre test dans with-postmortem
comme suit:
( deftest test-user-login
( with-postmortem *driver* { :dir " /path/to/folder " }
( doto *driver*
( go " http://127.0.0.1:8080 " )
( click-visible :login )
; ; any other actions...
)))
Maintenant que, si une exception se produit dans ce test, les artefacts seront enregistrés.
Pour ne pas copier et coller la carte des options, déclarez-la en haut du module. Si vous utilisez Circle CI, il serait formidable d'enregistrer les données dans un répertoire spécial Artefacts qui pourrait être téléchargé en tant que fichier zip une fois la construction terminée:
( def pm-dir
( or ( System/getenv " CIRCLE_ARTIFACTS " ) ; ; you are on CI
" /some/local/path " )) ; ; local machine
( def pm-opt
{ :dir pm-dir})
Maintenant, passez cette carte partout dans le gestionnaire PM:
; ; test declaration
( with-postmortem *driver* pm-opt
; ; test body goes here
)
Une fois qu'une erreur se produit, vous trouverez une image PNG qui représente votre page de navigateur au moment de l'exception et du vidage HTML.
Étant donné que les tests d'interface utilisateur peuvent prendre beaucoup de temps à passer, il est certainement une bonne pratique de passer les tests de serveur et d'interface utilisateur indépendamment les uns des autres.
Tout d'abord, ajoutez ^:integration
à tous les tests qui sont exécutés dans le navigateur comme suit:
( deftest ^:integration
test-password-reset-pipeline
( doto *driver*
( go url-password-reset)
( click :reset-btn )
...
Ensuite, ouvrez votre fichier project.clj
et ajoutez des sélecteurs de test:
:test-selectors { :default ( complement :integration )
:integration :integration }
Maintenant, une fois que vous aurez lancé lein test
vous exécuterez tous les tests sauf ceux du navigateur. Pour exécuter des tests d'intégration, lancez lein test :integration
.
La principale différence entre un programme et un humain est que le premier fonctionne très rapidement. Cela signifie si vite, qu'un navigateur ne peut parfois pas rendre de nouveaux HTML dans le temps. Donc, après chaque action, vous devez mettre une fonction wait-<something>
qui interroge simplement un navigateur vérifiant un prédicat. O Just (wait <seconds>)
Si vous ne vous souciez pas de l'optimisation.
Parfois, un fichier commence à télécharger automatiquement une fois que vous avez cliqué sur un lien ou simplement visiter une page. Dans les tests, vous devez vous assurer qu'un fichier a vraiment été téléchargé avec succès. Un scénario commun serait:
Exemple:
; ; Local helper that checks whether it is really an Excel file.
( defn xlsx? [file]
( -> file
.getAbsolutePath
( str/ends-with? " .xlsx " )))
; ; Top-level declarations
( def DL-DIR " /Users/ivan/Desktop " )
( def driver ( chrome { :download-dir DL-DIR}))
; ; Later, in tests...
( click-visible driver :download-that-application )
( wait driver 7 ) ; ; wait for a file has been downloaded
; ; Now, scan the directory and try to find a file:
( let [files ( file-seq ( io/file DL-DIR))
found ( some xlsx? files)]
( is found ( format " No *.xlsx file found in %s directory. " DL-DIR)))
Etaoin peut lire les fichiers produits par Selenium IDE. C'est un utilitaire officiel de créer des scénarios de manière interactive. L'IDE est une extension de votre navigateur. Une fois installé, il enregistre vos actions dans un fichier JSON avec l'extension .side
. Vous pouvez enregistrer ce fichier et l'exécuter avec Etaoin.
Imaginons que vous ayez installé l'IDE et enregistré certaines actions comme le prescrit la documentation officielle. Maintenant que vous avez un fichier test.side
, faites ceci:
( require '[etaoin.ide.flow :as flow])
( def driver ( chrome ))
( def ide-file ( io/resource " ide/test.side " ))
( def opt
{ ; ; The base URL redefines the one from the file.
; ; For example, the file was written on the local machine
; ; (http://localhost:8080), and we want to perform the scenario
; ; on staging (https://preprod-001.company.com)
:base-url " https://preprod-001.company.com "
; ; keywords :test-.. and :suite-.. (id, ids, name, names)
; ; are used to select specific tests. When not passed,
; ; all tests get run. For example:
:test-id " xxxx-xxxx... " ; ; a single test by its UUID
:test-name " some-test " ; ; a single test by its name
:test-ids [ " xxxx-xxxx... " , ...] ; ; multiple tests by their ids
:test-names [ " some-test1 " , ...] ; ; multiple tests by their names
; ; the same for suites:
:suite-id ...
:suite-name ...
:suite-ids [...]
:suite-names [...]})
( flow/run-ide-script driver ide-file opt)
Tout ce qui concerne l'IDE est stocké sous le package etaoin.ide
.
Vous pouvez également exécuter un script à partir de la ligne de commande. Voici l'exemple lein run
:
lein run -m etaoin.ide.main -d firefox -p ' {:port 8888 :args ["--no-sandbox"]} ' -r ide/test.side
Ainsi que d'un Uberjar. Dans ce cas, Etaoin doit être dans les dépendances primaires, et non les :dev
ou :test
liés.
java -cp .../poject.jar -m etaoin.ide.main -d firefox -p ' {:port 8888} ' -f ide/test.side
Nous prenons en charge les arguments suivants (vérifiez-les en utilisant la commande lein run -m etaoin.ide.main -h
):
-d, --driver-name name :chrome The name of driver. The default is `:chrome`
-p, --params params {} Parameters for the driver represented as an
EDN string, e.g '{:port 8080}'
-f, --file path Path to an IDE file on disk
-r, --resource path Path to an IDE resource
--test-ids ids Comma-separeted test ID(s)
--suite-ids ids Comma-separeted suite ID(s)
--test-names names Comma-separeted test name(s)
--suite-names names Comma-separeted suite name(s)
--base-url url Base URL for tests
-h, --help
Faites attention à la --params
. Il doit s'agir d'une chaîne EDN représentant une carte Clojure. C'est la même carte que vous passez dans un pilote lorsque vous l'initiez.
Veuillez noter que le support IDE est toujours expérimental. Si vous rencontrez un comportement inattendu, n'hésitez pas à ouvrir un problème. À l'heure actuelle, nous ne prenons en charge que Chrome et Firefox pour les fichiers IDE.
Exemple:
etaoin.api> ( def driver ( chrome ))
#'etaoin.api/driver
etaoin.api> ( maximize driver)
ExceptionInfo throw+: { :response {
:sessionId " 2672b934de785aabb730fd19330cf40c " ,
:status 13 ,
:value { :message " unknown error: cannot get automation extension n from unknown error: page could not be found: chrome-extension://aapnijgdinlhnhlmodcfapnahmbfebeb/_generated_background_page.html n
(Session info: chrome=57.0.2987.133) n (Driver info: chromedriver=2.27.440174
(e97a722caafc2d3a8b807ee115bfb307f7d2cfd9),platform=Mac OS X 10.11.6 x86_64) " }},
...
Solution: Mettez simplement à jour votre chromedriver
vers la dernière version. Testé avec 2.29, fonctionne bien. Les gens le disent également Woks depuis 2,28.
N'oubliez pas que brew
Package Manager a la version 2.27 obsolète. Vous devrez probablement télécharger des binaires à partir du site officiel.
Voir le problème connexe dans Selenium Project.
Lorsque vous passez une requête vectorielle, dites [{:tag :p} "//*[text()='foo')]]"}]
soyez prudent avec les expressions XPath écrites à la main. Dans Vector, chaque expression recherche de la précédente dans une boucle. Il y a une erreur cachée ici: sans point de premier plan, la clause "//..."
signifie trouver un élément de la racine de toute la page. Avec un point, il signifie trouver à partir du nœud actuel, qui est celui de la requête précédente, etc.
C'est pourquoi, il est facile de sélectionner quelque chose de complètement différent de ce que vous souhaitez. Une expression appropriée serait: [{:tag :p} ".//*[text()='foo')]]"}]
.
Exemple:
etaoin.api> ( click driver :some-id )
ExceptionInfo throw+: { :response {
:sessionId " d112ce8ddb49accdae78a769d5809eae " ,
:status 11 ,
:value { :message " element not visible n (Session info: chrome=57.0.2987.133) n
(Driver info: chromedriver=2.29.461585
(0be2cd95f834e9ee7c46bcc7cf405b483f5ae83b),platform=Mac OS X 10.11.6 x86_64) " }},
...
Solution: vous essayez de cliquer sur un élément qui n'est pas visible ou ses dimensions sont aussi peu que possible pour un humain de cliquer dessus. Vous devriez passer un autre sélecteur.
Problème: lorsque vous vous concentrez sur une autre fenêtre, la session WebDriver qui est exécutée sous Google Chrome échoue.
Solution: Google Chrome peut suspendre un onglet lorsqu'il est inactif depuis un certain temps. Lorsque la page est suspendue, aucune opération ne peut être effectuée dessus. Pas de clics, d'exécution JS, etc. Essayez donc de maintenir la fenêtre Chrome active pendant la session de test.
Problème: lorsque vous essayez de démarrer le pilote, vous obtenez une erreur:
user=> ( use 'etaoin.api)
user=> ( def driver ( firefox { :headless true }))
Erreur de syntaxe (exceptionInfo) Compilation à (REP: 1: 13). Throw +: {: Response {: Value {: Erreur "Erreur inconnue" ,: Message "Argument non valide: Impossible de tuer un processus quitté" .... ....
Cause possible: "L'exécution de Firefox en tant que root dans la session de l'utilisateur régulier n'est pas prise en charge"
Solution: Pour vérifier, exécutez le pilote avec le chemin d'accès aux fichiers de journal et le niveau de journal "Trace" et explorez leur sortie.
( def driver ( firefox { :log-stdout " ffout.log " :log-stderr " fferr.log " :driver-log-level " trace " }))
Problème similaire: Mozilla / Geckodriver # 1655
Problème: lorsque vous essayez de démarrer le ChromEdriver, vous obtenez une erreur:
Clojure.lang.ExceptionInfo: Throw +: {: Response {: SessionId "....." ,: Status 13 ,: Vale Le fichier n'existe pas) ...
Cause possible:
Une cause commune pour le chrome à planter pendant le démarrage est de l'exécution de Chrome en tant qu'utilisateur racine (administrateur) sur Linux. S'il est possible de contourner ce problème en passant à l'indicateur - no-sandbox lors de la création de votre session WebDriver, une telle configuration n'est pas supportée et très découragée. You need to configure your environment to run Chrome as a regular user instead.
Solution: Run driver with an argument --no-sandbox
. Prudence! This is a bypass OS security model.
( def driver ( chrome { :args [ " --no-sandbox " ]}))
A similar problem is described here
The etaoin.api2
namespace brings some bits of alternative macros and functions. They provide better syntax and live in a separate namespace to prevent the old API from breaking.
At the moment, the api2
module provides a set of with-...
macros with a let
-like binding form:
( ns ...
( :require
[etaoin.api :as api]
[etaoin.api2 :as api2]))
( api2/with-chrome [driver {}]
( api/go driver " http://ya.ru " ))
The options map can be skipped so you have only a binding symbol:
( api2/with-firefox [ff]
( api/go ff " http://ya.ru " ))
The project is open for your improvements and ideas. If any of unit tests fall on your machine please submit an issue giving your OS version, browser and console output.
Copyright © 2017—2020 Ivan Grishaev.
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.