Implementación pura de Clojure del protocolo WebDriver. Use esa biblioteca para automatizar un navegador, pruebe su comportamiento frontend, simule acciones humanas o lo que desee.
Lleva el nombre de Etaoin Shrdlu, una máquina de mecanografía que se puso viva después de que se produjo una nota de misterios.
Desde 0.4.0
, la instancia del controlador es un mapa pero no un átomo como solía ser. Fue una solución difícil para decidir, sin embargo, hemos librado de Atom para seguir a Clojure Way en nuestro código. En términos generales, nunca destaque un conductor ni almacena algo dentro de él. Todas las funciones internas que solían modificar la instancia ahora simplemente devuelven una nueva versión de un mapa. ¡Si tienes swap!
O algo similar en su código para el controlador, refactorice su código antes de actualizar.
Desde 0.4.0
, la biblioteca admite acciones webdriver. Las acciones son comandos enviados al controlador en lote. Consulte la sección detallada relacionada en TOC.
Desde 0.4.0
, Etaoin puede reproducir archivos de script creados en el IDE de selenio interactivo. Consulte la sección relacionada a continuación.
Ct t
como de costumbre.Puede enviar su empresa a esa lista.
Hay dos pasos para la instalación:
etaoin
en su código Clojure Agregue lo siguiente a :dependencies
Vector en su archivo project.clj
:
[etaoin "0.4.6"]
Funciona con Clojure 1.9 y superior.
Esta página proporciona instrucciones sobre cómo instalar controladores que necesita para automatizar su navegador.
Instale los navegadores de Chrome y Firefox que los descargan de los sitios oficiales. No habrá un problema en todas las plataformas.
Instale controladores específicos que necesite:
Driver de Google Chrome:
brew cask install chromedriver
para usuarios de Mac2.28
versión instalada. 2.27
y abajo tiene un error relacionado con la maximización de una ventana (ver [[Solución de problemas]]).Geckodriver, un conductor de Firefox:
brew install geckodriver
para usuarios de MacPhantom.js navegador:
brew install phantomjs
para usuarios de MacControlador de safari (solo para Mac):
Ahora, consulte su instalación que inicia cualquiera de estos comandos. Para cada comando, debe comenzar un proceso interminable con un servidor HTTP local.
chromedriver
geckodriver
phantomjs --wd
safaridriver -p 0
Puede ejecutar pruebas para el lanzamiento de esta biblioteca:
lein test
Verá las ventanas del navegador abiertas y cerradas en serie. Las pruebas utilizan un archivo HTML local con un diseño especial para validar la mayoría de los casos.
Consulte a continuación la sección de solución de problemas si tiene problemas
La buena noticia que puede automatizar su navegador directamente desde el replicador:
( 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)
Verá, cualquier función requiere una instancia del controlador como primer argumento. Por lo tanto, puede simplificarlo usando 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 ))
En ese caso, su código parece un DSL diseñado solo para tales fines.
Puede usar fill-multi
para acortar el código como:
( fill driver :login " login " )
( fill driver :password " pass " )
( fill driver :textarea " some text " )
en
( fill-multi driver { :login " login "
:password " pass "
:textarea " some text " })
Si ocurre alguna excepción durante una sesión de navegador, el proceso externo puede colgar para siempre hasta que la mate manualmente. Para prevenirlo, use with-<browser>
Macros de la siguiente manera:
( with-firefox {} ff ; ; additional options first, then bind name
( doto ff
( go " https://google.com " )
...))
Pase lo que pase durante una sesión, el proceso se detendrá de todos modos.
La mayoría de las funciones como click
, fill
, etc. requieren un término de consulta para descubrir un elemento en una página. Por ejemplo:
( click driver { :tag :button })
( fill driver { :id " searchInput " } " Clojure " )
La biblioteca admite los siguientes tipos y valores de consultas.
:active
significa el elemento activo actual. Al abrir la página de Google, por ejemplo, enfoca el cursor en la entrada de búsqueda principal. Por lo tanto, no hay necesidad de hacer clic manualmente. Ejemplo:
( fill driver :active " Let's search for something " keys/enter)
Cualquier otra palabra clave que indique la ID de un elemento. Para la página de Google, es :lst-ib
o "lst-ib"
(también son compatibles con las cadenas). El registro es importante. Ejemplo:
( fill driver :lst-ib " What is the Matrix? " keys/enter)
una cadena con una expresión de XPath. Tenga cuidado al escribirlos manualmente. Verifique la sección Troubleshooting
a continuación. Ejemplo:
( fill driver " .//input[@id='lst-ib'][@name='q'] " " XPath in action! " keys/enter)
Un mapa con :xpath
o :css
con una expresión de cadena de la sintaxis correspondiente. Ejemplo:
( fill driver { :xpath " .//input[@id='lst-ib'] " } " XPath selector " keys/enter)
( fill driver { :css " input#lst-ib[name='q'] " } " CSS selector " keys/enter)
Vea el manual del selector CSS para obtener más información.
Una consulta podría ser cualquier otro mapa que represente una expresión de XPath como datos. Las reglas son:
:tag
representa el nombre de una etiqueta. Se vuelve *
cuando no se pasa.:index
se expande en la cláusula de Trastring [x]
. Útil cuando necesita seleccionar una tercera fila de una tabla, por ejemplo.:fn/
espacio de nombres y se expande en algo específico.Ejemplos:
Encuentra la primera etiqueta div
( query driver { :tag :div })
; ; expands into .//div
Encuentra la etiqueta n-th div
( query driver { :tag :div :index 1 })
; ; expands into .//div[1]
Encuentre la etiqueta a
con el atributo de clase es igual a active
( query driver { :tag :a :class " active " })
; ; ".//a[@class="active"]"
Encuentra un formulario por sus atributos:
( query driver { :tag :form :method :GET :class :message })
; ; expands into .//form[@method="GET"][@class="message"]
Encuentre un botón por su texto (coincidencia exacta):
( query driver { :tag :button :fn/text " Press Me " })
; ; .//button[text()="Press Me"]
Encuentre un enésimo elemento ( p
, a
, lo que sea) con texto "Descargar":
( query driver { :fn/has-text " download " :index 3 })
; ; .//*[contains(text(), "download")][3]
Encuentra un elemento que tenga la siguiente clase:
( query driver { :tag :div :fn/has-class " overlay " })
; ; .//div[contains(@class, "overlay")]
Encuentre un elemento que tenga el siguiente dominio en un href:
( query driver { :tag :a :fn/link " google.com " })
; ; .//a[contains(@href, "google.com")]
Encuentra un elemento que tenga las siguientes clases a la vez:
( query driver { :fn/has-classes [ :active :sticky :marked ]})
; ; .//*[contains(@class, "active")][contains(@class, "sticky")][contains(@class, "marked")]
Encuentre los widgets de entrada habilitados/deshabilitados:
; ; 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()]
Una consulta podría ser un vector que consiste en cualquier expresión mencionada anteriormente. En tal consulta, cada próximo término busca una anterior recursivamente.
Un ejemplo simple:
( click driver [{ :tag :html } { :tag :body } { :tag :a }])
También puede combinar las expresiones XPATH y CSS (preste atención a un punto líder en la expresión de XPath:
( click driver [{ :tag :html } { :css " div.class " } " .//a[@class='download'] " ])
A veces es posible que deba interactuar con el enésimo elemento de una consulta, por ejemplo, cuando desea hacer clic en el segundo enlace en este ejemplo:
< 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 >
En este caso, puede usar la directiva :index
que es compatible con expresiones XPath como esta:
( click driver [{ :tag :li :class :search-result :index 2 } { :tag :a }])
O puede usar el truco enésimo hijo con la expresión de CSS como esta:
( click driver { :css " li.search-result:nth-child(2) a " })
Finalmente, también es posible obtener el enésimo elemento directamente utilizando query-all
:
( click-el driver ( nth ( query-all driver { :css " li.search-result a " }) 2 ))
Tenga en cuenta el uso de click-el
aquí, ya que query-all
devuelve un elemento, no un selector que se puede pasar para click
directamente.
query-tree
toma selectores y actúa como un árbol. Cada próximo selector consulta elementos de los anteriores. El selector FIST se basa en Find-Elements, y los RESS usan Find-Elements-From
( query-tree driver { :tag :div } { :tag :a })
medio
{:tag :div} -> [div1 div2 div3]
div1 -> [a1 a2 a3]
div2 -> [a4 a5 a6]
div3 -> [a7 a8 a9]
Entonces el resultado será [A1 ... A9]
Para interactuar con los elementos encontrados a través de una consulta, debe pasar el resultado de la consulta a click-el
o fill-el
:
( click-el driver ( first ( query-all driver { :tag :a })))
Por lo tanto, puede recolectar elementos en un vector e interactuar arbitrariamente con ellos en cualquier momento:
( 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! " )
Con el fin de emular la entrada humana, puede usar la función fill-human
. Las siguientes opciones están habilitadas de forma predeterminada:
{ :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
Y puedes redefinirlos:
( 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)
Para una entrada múltiple con emulación humana, use fill-human-multi
( fill-human-multi driver { :login " login "
:pass " password "
:textarea " some text " }
{ :mistake-prob 0.5
:pause-max 1 })
La función click
activa el clic izquierdo del mouse en un elemento encontrado por un término de consulta:
( click driver { :tag :button })
La función click
usa solo el primer elemento encontrado por la consulta, que a veces lleva a hacer clic en los elementos incorrectos. Para asegurarse de que se encuentre uno y solo un elemento, use la función click-single
. Actúa lo mismo pero plantea una excepción al consultar la página devuelve varios elementos:
( click-single driver { :tag :button :name " search " })
Un doble clic se usa raramente en la web, pero es posible con la función double-click
(Chrome, Phantom.js):
( double-click driver { :tag :dbl-click-btn })
También hay un montón de funciones de clic "ciegas". Activan los clics del mouse en la posición actual del mouse:
( left-click driver)
( middle-click driver)
( right-click driver)
Otro montón de funciones hace lo mismo, pero mueva el puntero del mouse a un elemento especificado antes de hacer clic en ellas:
( left-click-on driver { :tag :img })
( middle-click-on driver { :tag :img })
( right-click-on driver { :tag :img })
Un clic medio del mouse es útil al abrir un enlace en una nueva pestaña de fondo. El clic derecho a veces se utiliza para imitar un menú contextual en aplicaciones web.
La biblioteca admite acciones webdriver. En general, las acciones son comandos que describen dispositivos de entrada virtual.
{ :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 }]}]}
Puede crear un mapa manualmente y enviarlo al método 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)
o usar envoltorios. Primero debe crear un dispositivo de entrada virtual, por ejemplo:
( def keyboard ( make-key-input ))
y luego llénelo con las acciones necesarias:
( -> keyboard
( add-key-down keys/shift-left)
( add-key-down " a " )
( add-key-up " a " )
( add-key-up keys/shift-left))
Ejemplo extendido:
( 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))
Para borrar el estado de los dispositivos de entrada virtual, libere todas las teclas presionadas, etc., use el método release-actions
:
( release-actions driver)
Al hacer clic en un botón de entrada de archivo, abre un diálogo específico del sistema operativo con el que no se le permite interactuar con el protocolo WebDriver. Use la función upload-file
para adjuntar un archivo local a un widget de entrada de archivo. La función toma un selector que apunta a una entrada de archivo y una ruta completa como una cadena o una instancia nativa java.io.File
. El archivo debe existir o obtendrá una excepción de lo contrario. Ejemplo de uso:
( 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)
Llamar a una función screenshot
descarta la página actual en una imagen PNG en su disco:
( screenshot driver " page.png " ) ; ; relative path
( screenshot driver " /Users/ivan/page.png " ) ; ; absolute path
También es compatible con un objeto de archivo Java nativo:
; ; when imported as `[clojure.java.io :as io]`
( screenshot driver ( io/file " test.png " ))
; ; native object
( screenshot driver ( java.io.File. " test-native.png " ))
Con Firefox y Chrome, no puede capturar no toda la página, sino un solo elemento, digamos un DIV, un widget de entrada o lo que sea. No funciona con otros navegadores por ahora. Ejemplo:
( screenshot-element driver { :tag :div :class :smart-widget } " smart_widget.png " )
Con macro with-screenshots
, puede hacer una captura de pantalla después de cada forma
( with-screenshots driver " ../screenshots "
( fill driver :simple-input " 1 " )
( fill driver :simple-input " 2 " )
( fill driver :simple-input " 3 " ))
Lo que es equivalente a un registro:
( 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 " )
Recientemente, Google Chrome y luego Firefox comenzaron a admitir una característica llamada Modo sin cabeza. Cuando no tiene cabeza, ninguna de las ventanas de interfaz de usuario ocurre en la pantalla, solo la salida de Stdout entra en consola. Esta característica le permite ejecutar pruebas de integración en servidores que no tienen un dispositivo de salida gráfico.
Asegúrese de que su navegador admite el modo sin cabeza verificando si acepta el argumento de la línea de comandos --headles
al ejecutarlo desde el terminal. Phantom.js Driver no tiene cabeza por su naturaleza (nunca se ha desarrollado para representar la interfaz de usuario).
Al comenzar un controlador, pase :headless
para cambiar al modo sin cabeza. Nota, solo se admite la última versión de Chrome y Firefox. Para otros conductores, la bandera será ignorada.
( def driver ( chrome { :headless true })) ; ; runs headless Chrome
o
( def driver ( firefox { :headless true })) ; ; runs headless Firefox
Para verificar que se haya ejecutado cualquier controlador en modo sin cabeza, ¿usa headless?
predicado:
( headless? driver) ; ; true
Tenga en cuenta que siempre devolverá verdadero para las instancias Phantom.js.
Hay varios atajos para ejecutar Chrome o Firefox en modo sin cabeza de forma predeterminada:
( 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 " ))
También se encuentran when-headless
y when-not-headless
permiten realizar un montón de comandos solo si un navegador está en modo sin cabeza o no respectivamente:
( with-chrome nil driver
...
( when-not-headless driver
... some actions that might be not available in headless mode)
... common actions for both versions)
Para conectarse a un controlador que ya se ejecuta en un host local o remoto, debe especificar el parámetro :host
que podría ser un nombre de host (localhost, some.remote.host.net) o una dirección IP (127.0.0.1, 183.102.156.31 ) y el :port
. Si no se especifica el puerto, se establece el puerto predeterminado.
Ejemplo:
; ; 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
Para trabajar con el conductor en Docker, puede tomar imágenes preparadas:
Ejemplo para Chrome:
docker run --name chromedriver -p 9515:4444 -d -e CHROMEDRIVER_WHITELISTED_IPS='' robcherry/docker-chromedriver:latest
Para Firefox:
docker run --name geckodriver -p 4444:4444 -d instrumentisto/geckodriver
Para conectarse al controlador, solo necesita especificar el parámetro :host
como localhost
o 127.0.0.1
y el :port
en el que se ejecuta. Si no se especifica el puerto, se establece el puerto predeterminado.
( def driver ( chrome-headless { :host " localhost " :port 9515 :args [ " --no-sandbox " ]}))
( def driver ( firefox-headless { :host " localhost " })) ; ; will try to connect to port 4444
Para establecer la configuración del proxy, use variables de entorno HTTP_PROXY
/ HTTPS_PROXY
o pase un mapa del siguiente tipo:
{ :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 " }})
Nota: A: PAC-URL para un archivo de autoconfiguración proxy. Utilizado con Safari como otras opciones de proxy no funcionan en ese navegador.
Para ajustar el proxy, puede usar el objeto original y pasarlo a las capacidades:
{ :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 {...}}})
Con actualizaciones recientes, la biblioteca trae una gran característica. Ahora puede rastrear eventos que provienen del panel DevTools. Significa que todo lo que ves en la consola de desarrolladores ahora está disponible a través de API. Eso funciona solo con Google Chrome ahora.
Para iniciar un controlador con configuraciones de desarrollo especiales especificadas, simplemente pase un mapa vacío al campo :dev
cuando ejecute un controlador:
( def c ( chrome { :dev {}}))
El valor no debe ser nil
. Cuando se trata de un mapa vacío, una función especial toma valores predeterminados. Aquí hay una versión completa de la configuración de Dev con todos los valores posibles especificados.
( def c ( chrome { :dev
{ :perf
{ :level :all
:network? true
:page? true
:interval 1000
:categories [ :devtools
:devtools.network
:devtools.timeline ]}}}))
Debajo del capó, llena un diccionario especial perfLoggingPrefs
dentro del objeto chromeOptions
.
Ahora que su navegador acumula estos eventos, puede leerlos usando un espacio de nombres dev
especial.
( go c " http://google.com " )
; ; wait until the page gets loaded
; ; load the namespace
( require '[etaoin.dev :as dev])
Tengamos una lista de todas las solicitudes de HTTP que ocurrieron durante la página se cargó.
( 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 }
Dado que estamos principalmente interesados en las solicitudes de AJAX, hay una función get-ajax
que hace lo mismo pero las solicitudes de Filtros 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 patrón típico de uso de get-ajax
es el siguiente. Le gustaría verificar si se ha disparado una determinada solicitud al servidor. Por lo tanto, presiona un botón, espere un tiempo y luego lea las solicitudes realizadas por su navegador.
Al tener una lista de solicitudes, busca la que necesita (por ejemplo, por su URL) y luego verifique su estado. El :state
Field tiene la misma semántica como el XMLHttpRequest.readyState
. Es un entero de 1 a 4 con el mismo comportamiento.
Para verificar si una solicitud se ha terminado, realizada o fallida, use estos predicados:
( def req ( last reqs))
( dev/request-done? req)
; ; true
( dev/request-failed? req)
; ; false
( dev/request-success? req)
; ; true
¿Tenga en cuenta que request-done?
No significa que la solicitud haya tenido éxito. Solo significa que su tubería ha alcanzado un paso final.
ADVERTENCIA: Cuando lees los registros de desarrollo, los consumes de un búfer interno que se descarga. La segunda llamada a get-requests
o get-ajax
devolverá una lista vacía.
Quizás quieras recopilar estos registros por tu cuenta. Una función dev/get-performance-logs
devuelve una lista de registros para que los acumule en un átomo o lo que sea:
( def logs ( atom []))
; ; repeat that form from time to time
( do ( swap! logs concat ( dev/get-performance-logs c))
true )
( count @logs)
; ; 76
Hay logs->requests
y logs->ajax
Funciones que convierten los registros en solicitudes. A diferencia de get-requests
y get-ajax
, son funciones puras y no enjuagarán nada.
( dev/logs->requests @logs)
Cuando trabaje con registros y solicitudes, preste atención a su conteo y tamaño. Los mapas tienen muchas llaves y la cantidad de artículos en las colecciones podría ser enorme. Imprimir un montón de eventos puede congelar a su editor. Considere usar la función clojure.pprint/pprint
ya que se basa en los límites de nivel máximo y de longitud.
A veces, puede ser difícil descubrir qué salió mal durante la última sesión de pruebas de interfaz de usuario. Una macro especial with-postmortem
guarda algunos datos útiles en el disco antes de que se activara la excepción. Esos datos son una captura de pantalla, código HTML y registros de consola JS. Nota: No todos los navegadores admiten obtener registros JS.
Ejemplo:
( def driver ( chrome ))
( with-postmortem driver { :dir " /Users/ivan/artifacts " }
( click driver :non-existing-element ))
Una excepción aumentará, pero en /Users/ivan/artifacts
habrá tres archivos nombrados por una plantilla <browser>-<host>-<port>-<datetime>.<ext>
:
firefox-127.0.0.1-4444-2017-03-26-02-45-07.png
: una captura de pantalla real de la página del navegador;firefox-127.0.0.1-4444-2017-03-26-02-45-07.html
: el contenido HTML del navegador actual;firefox-127.0.0.1-4444-2017-03-26-02-45-07.json
: un archivo JSON con registros de consola; Esos son un vector de objetos.El controlador toma un mapa de opciones con las siguientes claves. Todos ellos pueden estar ausentes.
{ ; ; 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 " }
La función (get-logs driver)
devuelve los registros del navegador como vector de mapas. Cada mapa tiene la siguiente estructura:
{ :level :warning ,
:message " 1,2,3,4 anonymous (:1) " ,
:timestamp 1511449388366 ,
:source nil,
:datetime #inst " 2017-11-23T15:03:08.366-00:00 " }
Actualmente, los registros están disponibles solo en Chrome y Phantom.js. Tenga en cuenta que el texto del mensaje y el tipo de fuente dependen en gran medida del navegador. Chrome limpia los registros una vez que han sido leídos. Phantom.js los mantiene, pero solo hasta que cambie la página.
Al ejecutar una instancia de controlador, se puede pasar un mapa de parámetros adicionales para ajustar el comportamiento del navegador:
( def driver ( chrome { :path " /path/to/driver/binary " }))
A continuación, aquí hay un mapa de parámetros el soporte de la biblioteca. Todos ellos pueden ser omitidos o tienen valores nulos. Algunos de ellos, si no se pasan, se toman del mapa 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 " ]}}}
Cuando navega a una determinada página, el controlador espera hasta que toda la página se haya cargado por completo. Lo que está bien en la mayoría de los casos, pero no refleja la forma en que los seres humanos interactúan con Internet.
Cambie este comportamiento predeterminado con la opción :load-strategy
. Hay tres valores posibles para eso :: :none
,: :eager
y :normal
que es el valor predeterminado cuando no se pasa.
Cuando pasa :none
, el conductor responde de inmediato, por lo que puede ejecutar las siguientes instrucciones. Por ejemplo:
( def c ( chrome ))
( go c " http://some.slow.site.com " )
; ; you'll hang on this line until the page loads
( do-something )
Ahora al pasar la opción de estrategia de carga:
( def c ( chrome { :load-strategy :none }))
( go c " http://some.slow.site.com " )
; ; no pause, acts immediately
( do-something )
Para :eager
, solo funciona con Firefox en el momento de agregar la función a la biblioteca.
Hay una opción para ingresar una serie de claves simultáneamente. Eso es útil para imitar sosteniendo una clave del sistema como el control, el cambio o lo que sea al escribir.
El espacio de nombres etaoin.keys
lleva un montón de constantes clave, así como un conjunto de funciones relacionadas con la entrada.
( require '[etaoin.keys :as keys])
Un ejemplo rápido de ingresar a los caracteres ordinarios reteniendo el cambio:
( def c ( chrome ))
( go c " http://google.com " )
( fill-active c ( keys/with-shift " caps is great " ))
La entrada principal se llena con "Caps es genial". Ahora le gustaría eliminar la última palabra. En Chrome, esto se hace presionando Backspace Holting Alt. Hagamos eso:
( fill-active c ( keys/with-alt keys/backspace))
Ahora tienes solo "Caps es" en la entrada.
Considere un ejemplo más complejo que repite el comportamiento de los usuarios reales. Le gustaría eliminar todo de la entrada. Primero, mueves el careto al principio. Luego muévalo al final de retención de retención para que todo se seleccione. Finalmente, presiona Eliminar para borrar el texto seleccionado.
El combo es:
( fill-active c keys/home ( keys/with-shift keys/end) keys/delete)
También hay funciones with-ctrl
y with-command
que actúan igual.
Preste atención, estas funciones no se aplican a los atajos del navegador global. Por ejemplo, ni "comando + r" ni "comando + t" recargar la página ni abrir una nueva pestaña.
Todas las funciones keys/with-*
son solo envolturas sobre la función de keys/chord
que podrían usarse para casos complejos.
Para especificar su propio directorio donde descargar archivos, pase :download-dir
en un mapa de opción al ejecutar un controlador:
( def driver ( chrome { :download-dir " /Users/ivan/Desktop " }))
Ahora, una vez que haga clic en un enlace, se debe colocar un archivo en esa carpeta. Actualmente, solo se apoyan Chrome y Firefox.
Firefox requiere especificar los tipos MIME de aquellos archivos que deben descargarse sin mostrar un cuadro de diálogo del sistema. De manera predeterminada, cuando se pasa el parámetro :download-dir
, la biblioteca agrega los tipos mime más comunes: archivos, archivos multimedia, documentos de oficina, etc. Si necesita agregar su propia, anule esa preferencia manualmente:
( def driver ( firefox { :download-dir " /Users/ivan/Desktop "
:prefs { :browser.helperApps.neverAsk.saveToDisk
" some-mime/type-1;other-mime/type-2 " }}))
Para verificar si se descargó un archivo durante las pruebas de interfaz de usuario, consulte la sección de pruebas a continuación.
Establezca un encabezado de agente de usuario personalizado con la opción :user-agent
al crear un controlador, por ejemplo:
( def f ( firefox { :user-agent " Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) " }))
Para obtener el valor actual del encabezado en tiempo de ejecución, use la función:
( get-user-agent f)
; ; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Configurar ese encabezado es bastante importante para los navegadores sin cabeza, ya que la mayoría de los sitios verifican si el agente de usuario incluye la cadena "sin cabeza". Esto podría conducir a 403 respuesta o algún comportamiento extraño del sitio.
Al ejecutar Chrome o Firefox, puede especificar un perfil especial hecho para fines de prueba. Un perfil es una carpeta que mantiene la configuración del navegador, el historial, los marcadores y otros datos específicos del usuario.
Imagine que le gustaría ejecutar sus pruebas de integración contra un usuario que desactivó la ejecución de JavaScript o la representación de imágenes. Preparar un perfil especial para esa tarea sería una buena opción.
chrome://version/
página. Copie la ruta del archivo que está debajo del título de Profile Path
.-P
, -p
o -ProfileManager
como la página oficial describe.about:support
. Cerca del título Profile Folder
, presione el botón Show in Finder
. Debería aparecer una nueva ventana de carpeta. Copiar su camino desde allí. Una vez que tenga una ruta de perfil, inicie un controlador con una clave especial :profile
de la siguiente manera:
; ; 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 biblioteca envía un conjunto de funciones para desplazar la página.
El más importante, scroll-query
salta el primer elemento encontrado con el término de consulta:
( def driver ( chrome ))
; ; the form button placed somewhere below
( scroll-query driver :button-submit )
; ; the main article
( scroll-query driver { :tag :h1 })
Para saltar a la posición absoluta, solo use scroll
de la siguiente manera:
( scroll driver 100 600 )
; ; or pass a map with x and y keys
( scroll driver { :x 100 :y 600 })
Para desplazarse relativamente, use scroll-by
con valores de desplazamiento:
; ; keeps the same horizontal position, goes up for 100 pixels
( scroll-by driver 0 -100 ) ; ; map parameter is also supported
Hay dos atajos para saltar arriba o abajo de la página:
( scroll-bottom driver) ; ; you'll see the footer...
( scroll-top driver) ; ; ...and the header again
Las siguientes funciones desplazan la página en todas las direcciones:
( 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)
Una nota, en todos los casos, las acciones de desplazamiento se sirven con JavaScript. Asegúrese de que su navegador lo tenga habilitado.
Mientras trabaja con la página, no puede interactuar con los elementos que se colocan en un marco o un iframe. Las funciones a continuación cambian el contexto actual en un cuadro específico:
( 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
Los marcos podrían estar anidados uno en el otro. Las funciones tienen eso en cuenta. Digamos que tienes un diseño HTML como este:
< iframe src =" ... " >
< iframe src =" ... " >
< button id =" the-goal " >
</ frame >
</ frame >
Para que pueda alcanzar el botón con el siguiente código:
( 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
Para reducir el número de líneas de código, hay una macro especial with-frame
. Temporal cambia los marcos al ejecutar el cuerpo devuelve su última expresión y cambia a la trama anterior después.
( with-frame driver { :id :first-frame }
( with-frame driver { :id :nested-frame }
( click driver { :id :nested-button })
42 ))
El código anterior devuelve 42
permanecer en el mismo cuadro que ha sido antes antes de evaluar las macros.
Para evaluar un código JavaScript en un navegador, ejecute:
( js-execute driver " alert(1) " )
Puede pasar cualquier parámetro adicional a la llamada y cath en un script con el objeto de matriz arguments
:
( js-execute driver " alert(arguments[2].foo) " 1 false { :foo " hello! " })
Como resultado, hello!
La cadena aparecerá dentro del cuadro de diálogo.
Para devolver cualquier dato a Clojure, simplemente agregue return
a su script:
( js-execute driver " return {foo: arguments[2].foo, bar: [1, 2, 3]} "
1 false { :foo " hello! " })
; ; {:bar [1 2 3], :foo "hello!"}
Si su script realiza solicitudes AJAX u opera en setTimeout
o cualquier otra cosa de Async, no puede simplemente return
el resultado. En cambio, se debe llamar a una devolución de llamada especial contra los datos que desea lograr. El WebDriver pasa esta devolución de llamada como el último argumento para su script y podría ser contactado con el objeto de matriz arguments
.
Ejemplo:
( 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 }}})
Devuelve 42
al código Clojure.
Para evaluar un guión asincrónico, debe configurar un tiempo de espera especial para eso:
( set-script-timeout driver 5 ) ; ; in seconds
o envuelva el código en una macros que lo hace temporal:
( with-script-timeout driver 30
( js-async driver " some long script " ))
La principal diferencia entre un programa y un ser humano es que el primero opera muy rápido. Significa tan rápido que a veces un navegador no puede renderizar un nuevo HTML a tiempo. Entonces, después de cada acción, será mejor que ponga la función wait-<something>
que solo encuesta un navegador hasta que el predicado se evalúa en verdad. O simplemente (wait <seconds>)
si no te importa la optimización.
La macro with-wait
puede ser útil cuando necesite preparar cada acción con (wait n)
. Por ejemplo, la siguiente forma
( with-chrome {} driver
( with-wait 3
( go driver " http://site.com " )
( click driver { :id " search_button " })))
se convierte en algo como esto:
( with-chrome {} driver
( wait 3 )
( go driver " http://site.com " )
( wait 3 )
( click driver { :id " search_button " }))
y así devuelve el resultado de la última forma del cuerpo original.
Hay otra macro (doto-wait n driver & body)
que actúa como el doto
estándar pero prepende cada forma con (wait n)
. Por ejemplo:
( with-chrome {} driver
( doto-wait 1 driver
( go " http://site.com " )
( click :this-link )
( click :that-button )
...etc))
La forma final sería algo como esto:
( with-chrome {} driver
( doto driver
( wait 1 )
( go " http://site.com " )
( wait 1 )
( click :this-link )
( wait 1 )
( click :that-button )
...etc))
Además de with-wait
y do-wait
wait-has-alert
una serie de funciones wait-predicate
espera: wait-visible
Aceptan valores de tiempo de espera/intervalo predeterminados que se pueden redefinir utilizando las macros with-wait-timeout
y with-wait-interval
, respectivamente.
Ejemplo de la prueba de 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 " ))))
Texto de espera:
wait-has-text
espere hasta que un elemento tenga texto en cualquier lugar dentro de él (incluido HTML interno).
( wait-has-text driver :wait-span " -secret- " )
wait-has-text-everywhere
como wait-has-text
pero busca texto en toda la página
( wait-has-text-everywhere driver " -secret- " )
Para que su prueba no dependa unos de otros, debe envolverlos en un accesorio que cree una nueva instancia de un controlador y la apagará correctamente al final si cada prueba.
Una buena solución podría ser tener una variable global (sin dejar de acuerdo) que apuntará al controlador de destino durante las pruebas.
( 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 por alguna razón desea usar una sola instancia, puede usar accesorios como este:
( 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
Para pruebas más rápidas, puede usar este ejemplo:
.....
( 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 ))
......
En el ejemplo anterior, examinamos un caso cuando ejecuta pruebas contra un solo tipo de controlador. Sin embargo, es posible que desee probar su sitio en múltiples controladores, digamos, Chrome y Firefox. En ese caso, su accesorio puede volverse un poco más complejo:
( 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 ))))))
Ahora, cada prueba se ejecutará dos veces en los navegadores Firefox y Chrome. Tenga en cuenta que la llamada de prueba se previene con la macro testing
que coloca el nombre del controlador en el informe. Una vez que tenga un error, fácilmente encontrará qué controlador falló exactamente las pruebas.
Para guardar algunos artefactos en caso de excepción, envuelva el cuerpo de su prueba en el controlador with-postmortem
de la siguiente manera:
( 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...
)))
Ahora que, si se produce una excepción en esa prueba, se guardarán artefactos.
Para no copiar y pegar el mapa de opciones, declararlo en la parte superior del módulo. Si usa Circle CI, sería genial guardar los datos en un directorio de artefactos especiales que podría descargarse como un archivo zip una vez que se haya terminado la compilación:
( def pm-dir
( or ( System/getenv " CIRCLE_ARTIFACTS " ) ; ; you are on CI
" /some/local/path " )) ; ; local machine
( def pm-opt
{ :dir pm-dir})
Ahora pase ese mapa en todas partes en PM Handler:
; ; test declaration
( with-postmortem *driver* pm-opt
; ; test body goes here
)
Una vez que ocurre un error, encontrará una imagen PNG que representa la página de su navegador en el momento de la excepción y el volcado HTML.
Dado que las pruebas de interfaz de usuario pueden llevar mucho tiempo para pasar, definitivamente es una buena práctica pasar las pruebas de servidor y UI independientemente del otro.
Primero, agregue ^:integration
a todas las pruebas que se ejecutan en el navegador como sigue:
( deftest ^:integration
test-password-reset-pipeline
( doto *driver*
( go url-password-reset)
( click :reset-btn )
...
Luego, abra su archivo project.clj
y agregue selectores de prueba:
:test-selectors { :default ( complement :integration )
:integration :integration }
Ahora, una vez que inicie lein test
ejecutará todas las pruebas, excepto las del navegador. Para ejecutar pruebas de integración, inicie lein test :integration
.
La principal diferencia entre un programa y un humano es que el primero opera muy rápido. Significa tan rápido que a veces un navegador no puede renderizar un nuevo HTML a tiempo. Entonces, después de cada acción, debe colocar la función wait-<something>
que solo encueste un revuelto del navegador para un predicado. O Solo (wait <seconds>)
si no te importa la optimización.
A veces, un archivo comienza a descargarse automáticamente una vez que ha hecho clic en un enlace o simplemente visitó alguna página. En las pruebas, debe asegurarse de que un archivo realmente se haya descargado con éxito. Un escenario común sería:
Ejemplo:
; ; 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 puede reproducir los archivos producidos por Selenium IDE. Es una utilidad oficial para crear escenarios de manera interactiva. El IDE viene como una extensión de su navegador. Una vez instalado, registra sus acciones en un archivo JSON con la extensión .side
. Puede guardar ese archivo y ejecutarlo con Etaoin.
Imaginemos que ha instalado el IDE y registró algunas acciones como prescribe la documentación oficial. Ahora que tiene un archivo test.side
, haga esto:
( 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)
Todo lo relacionado con el IDE se almacena bajo el paquete etaoin.ide
.
También puede ejecutar un script desde la línea de comando. Aquí está el ejemplo lein run
:
lein run -m etaoin.ide.main -d firefox -p ' {:port 8888 :args ["--no-sandbox"]} ' -r ide/test.side
Así como de un Uberjar. En este caso, Etaoin debe estar en las dependencias principales, no en :dev
o :test
Related.
java -cp .../poject.jar -m etaoin.ide.main -d firefox -p ' {:port 8888} ' -f ide/test.side
Apoyamos los siguientes argumentos (échales un vistazo usando el comando 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
Presta atención al --params
uno. Esta debe ser una cadena EDN que represente un mapa de Clojure. Ese es el mismo mapa que pasa a un conductor cuando lo inicia.
Tenga en cuenta que el soporte IDE sigue siendo experimental. Si se encuentra con un comportamiento inesperado, no dude en abrir un problema. Por el momento, solo admitimos Chrome y Firefox para archivos IDE.
Ejemplo:
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) " }},
...
Solución: simplemente actualice su chromedriver
a la última versión. Probado con 2.29, funciona bien. La gente también lo dice desde 2.28.
Recuerde, brew
Package Manager tiene la versión anticuada 2.27. Probablemente tendrá que descargar binarios del sitio oficial.
Vea el problema relacionado en el proyecto Selenium.
Al pasar una consulta similar al vector, digamos [{:tag :p} "//*[text()='foo')]]"}]
Tenga cuidado con las expresiones XPath escritas a mano. En Vector, cada su expresión busca la anterior en un bucle. Aquí hay un error oculto: sin un punto principal, la cláusula "//..."
significa encontrar un elemento de la raíz de toda la página. Con un punto, significa encontrar desde el nodo actual, que es uno de la consulta anterior, y así sucesivamente.
Por eso, es fácil seleccionar algo completamente diferente que lo que le gustaría. Una expresión adecuada sería: [{:tag :p} ".//*[text()='foo')]]"}]
.
Ejemplo:
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) " }},
...
Solución: está tratando de hacer clic en un elemento que no es visible o sus dimensiones son tan poco como imposibles para un humano hacer clic en él. Debes pasar otro selector.
Problema: Cuando se enfoca en otra ventana, la sesión de WebDriver que se ejecuta en Google Chrome falla.
Solución: Google Chrome puede suspender una pestaña cuando ha estado inactivo durante algún tiempo. Cuando la página está suspendida, no se pudo realizar una operación. No hay clics, ejecución de JS, etc. Así que intente mantener activa la ventana de Chrome durante la sesión de prueba.
Problema: cuando intenta iniciar el controlador, recibe un error:
user=> ( use 'etaoin.api)
user=> ( def driver ( firefox { :headless true }))
Error de sintaxis (ExceptionInfo) Compilación en (Repliate: 1: 13). Throw+: {: respuesta {: valor {: Error "Error desconocido" ,: Mensaje "Argumento no válido: No se puede matar un proceso salido" ....
Causa posible: "Ejecutar Firefox como root en una sesión de usuario regular no es compatible"
Solución: para verificar, ejecute el controlador con la ruta a los archivos de registro y el nivel de registro de "trazar" y explore su salida.
( def driver ( firefox { :log-stdout " ffout.log " :log-stderr " fferr.log " :driver-log-level " trace " }))
Problema similar: Mozilla/Geckodriver#1655
Problema: cuando intenta iniciar el cromedriver, recibe un error:
clojure.lang.exceptionInfo: throw+: {: respuesta {: sessionId "....." ,: status 13 ,: valor {: mensaje "Error desconocido: Chrome no pudo comenzar: salió anormalmente. n (error desconocido: devitoolsactiveport El archivo no existe) ...
Posible causa:
Una causa común para que Chrome se bloquee durante el inicio es ejecutar Chrome como usuario root (administrador) en Linux. Si bien es posible trabajar en torno a este problema pasando-No-sandbox indicador al crear su sesión de WebDriver, dicha configuración no es compatible y está muy desanimada. You need to configure your environment to run Chrome as a regular user instead.
Solution: Run driver with an argument --no-sandbox
. ¡Precaución! 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.