Pure Clojure implementation of Webdriver protocol. Use that library to automate a browser, test your frontend behaviour, simulate human actions or whatever you want.
It's named after Etaoin Shrdlu -- a typing machine that became alive after a mysteries note was produced on it.
- Benefits
- Capabilities
- Who uses it?
- Documentation
- Installation
- Basic Usage
- Advanced Usage
- Writing Integration Tests For Your Application
- Installing Drivers
- Troubleshooting
- Contributors
- License
- Selenium-free: no long dependencies, no tons of downloaded jars, etc.
- Lightweight, fast. Simple, easy to understand.
- Compact: just one main module with a couple of helpers.
- Declarative: the code is just a list of actions.
- Currently supports Chrome, Firefox, Phantom.js and Safari (partially).
- May either connect to a remote driver or run it on your local machine.
- Run your unit tests directly from Emacs pressing
C-t t
as usual. - Can imitate human-like behaviour (delays, typos, etc).
You are welcome to submit your company into that list.
Add the following into :dependencies
vector in your project.clj
file:
[etaoin "0.1.8-SNAPSHOT"]
The good news you may automate your browser directly from the REPL:
(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})
;; 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)
You see, any function requires a driver instance as the first argument. So you
may simplify it using doto
macros:
(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})
...
(wait-visible {:id :firstHeading})
(get-url)
"https://en.wikipedia.org/wiki/Clojure"
...
(quit))
In that case, your code looks like a DSL designed just for such purposes.
If any exception occurs during a browser session, the external process might
hang forever until you kill it manually. To prevent it, use with-<browser>
macros as follows:
(with-firefox {} ff ;; additional options first, then bind name
(doto ff
(go "https://google.com")
...))
Whatever happens during a session, the process will be stopped anyway.
Most of the functions work with a term that return first single element. For
example, (click driver {:tag :div})
will click on the first div
tag found on
the page. Therefore it's better to operate on element's IDs rather then classes
to prevent strange behaviour.
In case your really need to get multiple elements and process them in batch, use
query-all
function with other ones that names end with -el
. These functions
are to work with machine wise elements represented by long driver-specific
string values.
Here is a example of how to get all the links from the page:
(def driver (firefox))
(go driver "http://wikipedia.org")
(let [els (query-all driver {:tag :a})
;; els is vector of strings smth like "280abeaf-27ec-5544-8634-b2cfe86a58a6"
get-link #(get-element-attr-el driver % :href)]
(mapv get-link els))
;; returns ["//ru.wikipedia.org/" "//en.wikipedia.org/" etc ... ]
Since version 59, Google Chrome officially supports headless mode. It's when it works without opening a UI window so it is possible to run such a driver on servers without a display device.
Headless mode uses the standard chromedriver
, the difference is only in
additional parameters passed to Chrome.
To use headless driver in your code, use either (headless)
function or
(with-headless)
macros as well. Perhaps you will need to take more screenshots
to see that's going on under the hood:
(doto driver
;; ... clicks, etc
(when-headless
(screenshot driver "/in/the/middle/of/test.png"))
;; continue
)
Sometimes, it might be difficult to discover what went wrong during the last UI
tests session. To keep some postmortem evident on your disk, wrap the code
block with with-postmortem
macros:
(def driver (firefox))
(with-postmortem driver {:dir "/Users/ivan/artifacts"}
(click driver :non-existing-element))
An exception will rise, but in /Users/ivan/artifacts
there will be two files:
-
firefox-127.0.0.1-4444-2017-03-26-02-45-07.png
: an actual screenshot of the browser's page; -
firefox-127.0.0.1-4444-2017-03-26-02-45-07.html
: an actual browser's HTML content.
The filename template is <browser>-<host>-<port>-<datetime>.ext
.
The main difference between a program and a human is that the first one
operates very fast. It means so fast, that sometimes a browser cannot render new
HTML in time. So after each action you need to put wait-<something>
function
that just polls a browser checking for a predicate. O just (wait <seconds>)
if
you don't care about optimization.
The with-wait
macro might be helpful when you need to prepend each action
with (wait n)
. For example, the following form
(with-chrome {} driver
(with-wait 3
(go driver "http://site.com")
(click driver {:id "search_button"})))
turns into something like this:
(with-chrome {} driver
(wait 3)
(go driver "http://site.com")
(wait 3)
(click driver {:id "search_button"}))
and thus returns the result of the last form of the original body.
To make your test not depend on each other, you need to wrap them into a fixture that will create a new instance of a driver and shut it down properly at the end if each test.
Good solution might be to have a global variable (unbound by default) that will point to the target driver during the tests.
(ns project.test.integration
"A module for integration tests"
(:require [clojure.test :refer :all]
[etaoin.api :refer :all]))
(def ^:dynamic
"Current driver"
*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)
...
))
In the example above, we examined a case when you run tests against a single type of driver. However, you may want to test your site on multiple drivers, say, Chrome and Firefox. In that case, your fixture may become a bit more complex:
(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))))))
Now, each test will be run twice in both Firefox and Chrome browsers. Please
note the test call is prepended with testing
macro that puts driver name into
the report. Once you've got an error, you'll easy find what driver failed the
tests exactly.
To save a screenshot and HTML dump of your page in case of exception, wrap your
fixture into with-postmortem
handler as follows:
(defn fixture-drivers [f]
(doseq [type driver-types]
(with-driver type {} driver
(with-postmortem driver {:dir "/path/to/folder"}
(binding [*driver* driver]
(testing (format "Testing in %s browser" (name type))
(f)))))))
If you use Circle CI, it would be great to save data into artifacts directory:
(def pm-dir
(or (System/getenv "CIRCLE_ARTIFACTS")
"/some/local/path"))
Now pass pm-dir
into with-postmortem
macro:
...
(with-postmortem driver {:dir pm-dir}
(binding [*driver* driver]
...
Once an error occurs, you will find a PNG image that represents your browser page at the moment of exception and HTML dump.
Since UI tests may take lots of time to pass, it's definitely a good practice to pass both server and UI tests independently from each other.
First, add ^:integration
tag to all the tests that are run inder the browser
like follows:
(deftest ^:integration
test-password-reset-pipeline
(doto *driver*
(go url-password-reset)
(click :reset-btn)
...
Then, open your project.clj
file and add test selectors:
:test-selectors {:default (complement :integration)
:integration :integration}
Now, once you launch lein test
you will run all the tests except browser
ones. To run integration tests, launch lein test :integration
.
The main difference between a program and a human is that the first one
operates very fast. It means so fast, that sometimes a browser cannot render new
HTML in time. So after each action you need to put wait-<something>
function
that just polls a browser checking for a predicate. O just (wait <seconds>)
if
you don't care about optimization.
This page provides instructions on how to install drivers you need to automate your browser.
Install Chrome and Firefox browsers downloading them from the official sites. There won't be a problem on all the platforms.
Install specific drivers you need:
-
Google Chrome driver:
brew install chromedriver
for Mac users- or download compiled binaries from the official site.
- ensure you have at least
2.28
version installed.2.27
and below has a bug related to maximizing a window (see [[Troubleshooting]]).
-
Geckodriver, a driver for Firefox:
brew install geckodriver
for Mac users- or download it from the official Mozilla site.
-
Phantom.js browser:
brew install phantomjs
For Mac users- or download it from the official site.
-
Safari Driver (for Mac only):
- update your Mac OS to El Captain using App Store;
- set up Safari options as the Webkit page says (scroll down to "Running the Example in Safari" section).
Now, check your installation launching any of these commands. For each command, an endless process with a local HTTP server should start.
chromedriver
geckodriver
phantomjs --wd
safaridriver -p 0
You may run tests for this library launching:
lein test
You'll see browser windows open and close in series. The tests use a local HTML file with a special layout to validate the most of the cases.
This page holds common troubles you might face during webdriver automation.
Example:
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\nfrom 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: just update your chromedriver
to the last version. Tested with
2.29, works fine. People say it woks as well since 2.28.
Remember, brew
package manager has the outdated version 2.27. You will
probably have to download binaries from the official site.
See the related issue in Selenium project.
Example:
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: you are trying to click an element that is not visible or its dimentions are as little as it's impossible for a human to click on it. You should pass another selector.
Problem: when you focus on other window, webdriver session that is run under Google Chrome fails.
Solution: Google Chrome may suspend a tab when it has been inactive for some time. When the page is suspended, no operation could be done on it. No clicks, Js execution, etc. So try to keep Chrome window active during test session.
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 Ivan Grishaev.
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.