diff --git a/README.md b/README.adoc similarity index 52% rename from README.md rename to README.adoc index 05e3e444..fe722f5b 100644 --- a/README.md +++ b/README.adoc @@ -1,231 +1,158 @@ -[url-webdriver]: https://www.w3.org/TR/webdriver/ -[url-wiki]: https://en.wikipedia.org/wiki/Etaoin_shrdlu#Literature -[url-tests]: https://github.com/igrishaev/etaoin/blob/master/test/etaoin/api_test.clj -[url-doc]: http://etaoin.grishaev.me/ -[url-slack]: https://clojurians.slack.com/messages/C7KDM0EKW/ - -# Etaoin - -[![CircleCI](https://circleci.com/gh/igrishaev/etaoin.svg?style=svg)](https://circleci.com/gh/igrishaev/etaoin) - -Pure Clojure implementation of [Webdriver][url-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][url-wiki] -- a typing machine that became alive -after a mysteries note was produced on it. - -## Release Notes - -### Atom turns into a map - -Since `0.4.0`, the driver instance is **a map but not an atom** like -it used to be. It was a difficult solution to decide on, yet we've got rid of -atom to follow Clojure way in our code. Generally speaking, you never deref a -driver or store something inside it. All the internal functions that used to -modify the instance now just return a new version of a map. If you have `swap!` -or something similar in your code for the driver, please refactor your code -before you update. - -[actions]: https://www.w3.org/TR/webdriver/#actions - -### Actions - -Since `0.4.0`, the library supports [Webdriver -Actions][actions]. Actions are commands sent to the driver in batch. See the -detailed related section in ToC. - -### Selenium IDE support - -[ide]: https://www.selenium.dev/selenium-ide/ - -Since `0.4.0`, Etaoin can play script files created in the interactive -[Selenium IDE][ide]. See the related section below. - -# Table of Contents - - - -- [Benefits](#benefits) -- [Capabilities](#capabilities) -- [Who uses it?](#who-uses-it) -- [Documentation](#documentation) -- [Installation](#installation) - * [Installing the etaoin library](#installing-the-etaoin-library) - * [Installing the Browser Drivers](#installing-the-browser-drivers) -- [Getting started](#getting-started) -- [Querying elements](#querying-elements) - * [Simple queries, XPath, CSS](#simple-queries-xpath-css) - * [Map syntax for querying](#map-syntax-for-querying) - * [Vector syntax for querying](#vector-syntax-for-querying) - * [Advanced queries](#advanced-queries) - + [Querying the *nth* element matched](#querying-the-nth-element-matched) - + [Getting elements like in a tree](#getting-elements-like-in-a-tree) - * [Interacting with queried elements](#interacting-with-queried-elements) -- [Emulation of human input](#emulation-of-human-input) -- [Mouse clicks](#mouse-clicks) -- [Actions](#actions) -- [File uploading](#file-uploading) -- [Screenshots](#screenshots) - * [Screening elements](#screening-elements) - * [Screening after each form](#screening-after-each-form) -- [Using headless drivers](#using-headless-drivers) -- [Connection to remote webdriver](#connection-to-remote-webdriver) -- [Webdriver in Docker](#webdriver-in-docker) -- [HTTP Proxy](#http-proxy) -- [Devtools: tracking HTTP requests, XHR (Ajax)](#devtools-tracking-http-requests-xhr-ajax) -- [Postmortem: auto-save artifacts in case of exception](#postmortem-auto-save-artifacts-in-case-of-exception) -- [Reading browser's logs](#reading-browsers-logs) -- [Additional parameters](#additional-parameters) -- [Eager page load](#eager-page-load) -- [Keyboard chords](#keyboard-chords) -- [File download directory](#file-download-directory) -- [Managing User-Agent](#managing-user-agent) -- [Setting browser profile](#setting-browser-profile) - * [Create and find a profile in Chrome](#create-and-find-a-profile-in-chrome) - * [Create and find a profile in Firefox](#create-and-find-a-profile-in-firefox) - * [Running a driver with a profile](#running-a-driver-with-a-profile) -- [Scrolling](#scrolling) -- [Working with frames and iframes](#working-with-frames-and-iframes) -- [Executing Javascript](#executing-javascript) - * [Asynchronous scripts](#asynchronous-scripts) -- [Wait functions](#wait-functions) -- [Writing Integration Tests For Your Application](#writing-integration-tests-for-your-application) - * [Basic fixture](#basic-fixture) - * [Multi-Driver Fixtures](#multi-driver-fixtures) - * [Postmortem Handler To Collect Artifacts](#postmortem-handler-to-collect-artifacts) - * [Running Tests By Tag](#running-tests-by-tag) - * [Check whether a file has been downloaded](#check-whether-a-file-has-been-downloaded) -- [Running IDE files (new!)](#running-ide-files-new) - * [CLI arguments](#cli-arguments) -- [Troubleshooting](#troubleshooting) - * [Calling maximize function throws an error](#calling-maximize-function-throws-an-error) - * [Querying wrong elements with XPath expressions](#querying-wrong-elements-with-xpath-expressions) - * [Clicking On Non-Visible Element](#clicking-on-non-visible-element) - * [Unpredictable errors in Chrome when window is not active](#unpredictable-errors-in-chrome-when-window-is-not-active) - * [Invalid argument: can't kill an exited process](#invalid-argument-cant-kill-an-exited-process) - * [DevToolsActivePort file doesn't exist](#devtoolsactiveport-file-doesnt-exist) -- [API v2](#api-v2) -- [Contributors](#contributors) -- [Other materials](#other-materials) -- [License](#license) - - - -## Benefits - -- 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. - -## Capabilities - -- Currently supports Chrome, Firefox, 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). - -## Who uses it? - -- [Exoscale](https://www.exoscale.com/) -- [Flyerbee](https://www.flyerbee.com/) -- [Roomkey](https://www.roomkey.com/) -- [Barrick Gold](http://www.barrick.com/) -- [Doctor Evidence](http://drevidence.com/) -- [Kevel (formerly Adzerk)](https://kevel.com/) -- [Guaranteed Rate](https://www.rate.com/) += Etaoin +:toc: macro +:toclevels: 4 +:project-src-coords: clj-commons/etaoin +:project-mvn-coords: etaion/etaoin +:url-webdriver: https://www.w3.org/TR/webdriver/ +:url-wiki: https://en.wikipedia.org/wiki/Etaoin_shrdlu#Literature +:url-tests: https://github.com/igrishaev/etaoin/blob/master/test/etaoin/api_test.clj +:url-doc: http://etaoin.grishaev.me/ +:url-slack: https://clojurians.slack.com/messages/C7KDM0EKW/ -You are welcome to submit your company into that list. +// Badges +https://circleci.com/gh/{project-src-coords}[image:https://circleci.com/gh/{project-src-coords}.svg?style=svg[CircleCI]] -## Documentation +Pure Clojure implementation of link:{url-webdriver}[Webdriver] protocol. +Use that library to automate a browser, test your frontend behaviour, simulate human actions or whatever you want. -- [API docs][url-doc] -- [Slack channel][url-slack] -- [Unit tests][url-tests] +It's named after link:{url-wiki}[Etaoin Shrdlu] -- a typing machine that became alive after a mysteries note was produced on it. -## Installation +== Release Notes -There are two steps to installation: - 1. Install the library `etaoin` into your clojure code - 2. Install the drivers for each browser +=== Atom turns into a map -### Installing the etaoin library -Add the following into `:dependencies` vector in your `project.clj` file: +Since `0.4.0`, the driver instance is *a map but not an atom* like it used to be. +It was a difficult solution to decide on, yet we've got rid of atom to follow Clojure way in our code. +Generally speaking, you never deref a driver or store something inside it. +All the internal functions that used to modify the instance now just return a new version of a map. +If you have `swap!` or something similar in your code for the driver, please refactor your code before you update. -``` -[etaoin "0.4.6"] -``` +=== Actions +:actions: https://www.w3.org/TR/webdriver/#actions -Works with Clojure 1.9 and above. +Since `0.4.0`, the library supports link:{actions}[Webdriver Actions]. +Actions are commands sent to the driver in batch. +See the detailed related section in ToC. -### Installing the Browser Drivers +=== Selenium IDE support +:ide: https://www.selenium.dev/selenium-ide/ -[url-webdriver]: https://www.w3.org/TR/webdriver/ -[url-tests]: https://github.com/igrishaev/etaoin/blob/master/test/etaoin/api_test.clj -[url-chromedriver]: https://sites.google.com/a/chromium.org/chromedriver/ -[url-chromedriver-dl]: https://sites.google.com/a/chromium.org/chromedriver/downloads -[url-geckodriver-dl]: https://github.com/mozilla/geckodriver/releases -[url-phantom-dl]: http://phantomjs.org/download.html -[url-webkit]: https://webkit.org/blog/6900/webdriver-support-in-safari-10/ +Since `0.4.0`, Etaoin can play script files created in the interactive link:{ide}[Selenium IDE]. +See the related section below. -This page provides instructions on how to install drivers you need to automate -your browser. +toc::[] -Install Chrome and Firefox browsers downloading them from the official -sites. There won't be a problem on all the platforms. +== Benefits -Install specific drivers you need: +* 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. + +== Capabilities + +* Currently supports Chrome, Firefox, 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). + +== Who uses it? -- Google [Chrome driver][url-chromedriver]: +* https://www.exoscale.com/[Exoscale] +* https://www.flyerbee.com/[Flyerbee] +* https://www.roomkey.com/[Roomkey] +* http://www.barrick.com/[Barrick Gold] +* http://drevidence.com/[Doctor Evidence] +* https://kevel.com/[Kevel (formerly Adzerk)] +* https://www.rate.com/[Guaranteed Rate] - - `brew cask install chromedriver` for Mac users - - or download compiled binaries from the [official site][url-chromedriver-dl]. - - ensure you have at least `2.28` version installed. `2.27` and below has a - bug related to maximizing a window (see [[Troubleshooting]]). +You are welcome to submit your company into that list. + +== Documentation -- Geckodriver, a driver for Firefox: +* link:{url-doc}[API docs] +* link:{url-slack}[Slack channel] +* link:{url-tests}[Unit tests] - - `brew install geckodriver` for Mac users - - or download it from the official [Mozilla site][url-geckodriver-dl]. +== Installation -- Phantom.js browser (obsolete, no longer tested): +There are two steps to installation: - - `brew install phantomjs` For Mac users - - or download it from the [official site][url-phantom-dl]. +. Install the library `etaoin` into your clojure code +. Install the drivers for each browser -- Safari Driver (for Mac only): +=== Installing the etaoin library + +Add the following into `:dependencies` vector in your `project.clj` file: - - update your Mac OS to El Captain using App Store; - - set up Safari options as the [Webkit page][url-webkit] says (scroll down to - "Running the Example in Safari" section). +---- +[etaoin "0.4.6"] +---- + +:url-webdriver: https://www.w3.org/TR/webdriver/ +:url-tests: https://github.com/igrishaev/etaoin/blob/master/test/etaoin/api_test.clj +:url-chromedriver: https://sites.google.com/a/chromium.org/chromedriver/ +:url-chromedriver-dl: https://sites.google.com/a/chromium.org/chromedriver/downloads +:url-geckodriver-dl: https://github.com/mozilla/geckodriver/releases +:url-phantom-dl: http://phantomjs.org/download.html +:url-webkit: https://webkit.org/blog/6900/webdriver-support-in-safari-10/ + +Works with Clojure 1.9 and above. -Now, check your installation launching any of these commands. For each command, -an endless process with a local HTTP server should start. +=== Installing the Browser Drivers -```bash +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 link:{url-chromedriver}[Chrome driver]: + ** `brew cask install chromedriver` for Mac users + ** or download compiled binaries from the link:{url-chromedriver-dl}[official site]. + ** ensure you have at least `2.28` version installed. +`2.27` and below has a bug related to maximizing a window (see <>). +* Geckodriver, a driver for Firefox: + ** `brew install geckodriver` for Mac users + ** or download it from the official link:{url-geckodriver-dl}[Mozilla site]. +* Phantom.js browser (obsolete, no longer tested): + ** `brew install phantomjs` For Mac users + ** or download it from the link:{url-phantom-dl}[official site]. +* Safari Driver (for Mac only): + ** update your Mac OS to El Captain using App Store; + ** set up Safari options as the link:{url-webkit}[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. + +[source,bash] +---- chromedriver geckodriver phantomjs --wd safaridriver -p 0 -``` +---- You may run tests for this library launching: -```bash +[source,bash] +---- 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. +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. -See below for the [Troubleshooting section](https://github.com/igrishaev/etaoin#troubleshooting) if you have problems +See below for <> if you have problems -## Getting started +== Getting started The good news you may automate your browser directly from the REPL: -```clojure +[source,clojure] +---- (use 'etaoin.api) (require '[etaoin.keys :as k]) @@ -270,12 +197,13 @@ The good news you may automate your browser directly from the REPL: ;; 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: +You see, any function requires a driver instance as the first argument. +So you may simplify it using `doto` macros: -```clojure +[source,clojure] +---- (def driver (firefox)) (doto driver (go "https://en.wikipedia.org/") @@ -288,203 +216,228 @@ may simplify it using `doto` macros: (wait-visible {:id :firstHeading}) ;; ... (quit)) -``` +---- In that case, your code looks like a DSL designed just for such purposes. You can use `fill-multi` to shorten the code like: -``` clojure +[source,clojure] +---- (fill driver :login "login") (fill driver :password "pass") (fill driver :textarea "some text") -``` +---- into -``` clojure +[source,clojure] +---- (fill-multi driver {:login "login" :password "pass" :textarea "some text"}) -``` +---- -If any exception occurs during a browser session, the external process might -hang forever until you kill it manually. To prevent it, use `with-` -macros as follows: +If any exception occurs during a browser session, the external process might hang forever until you kill it manually. +To prevent it, use `with-` macros as follows: -```clojure +[source,clojure] +---- (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. -## Querying elements +== Querying elements -Most of the functions like `click`, `fill`, etc require a query term to discover -an element on a page. For example: +Most of the functions like `click`, `fill`, etc require a query term to discover an element on a page. +For example: -```clojure +[source,clojure] +---- (click driver {:tag :button}) (fill driver {:id "searchInput"} "Clojure") -``` - -[xpath-sel]:https://www.w3schools.com/xml/xpath_syntax.asp -[css-sel]:https://www.w3schools.com/cssref/css_selectors.asp +---- The library supports the following query types and values. -### Simple queries, XPath, CSS - -- `:active` stands for the current active element. When opening Google page for - example, it focuses the cursor on the main search input. So there is no need - to click on in manually. Example: - - ```clojure - (fill driver :active "Let's search for something" keys/enter) - ``` - -- any other keyword that indicates an element's ID. For Google page, it is - `:lst-ib` or `"lst-ib"` (strings are also supported). The registry - matters. Example: - - ```clojure - (fill driver :lst-ib "What is the Matrix?" keys/enter) - ``` -- a string with an [XPath][xpath-sel] expression. Be careful when writing them - manually. Check the `Troubleshooting` section below. Example: +=== Simple queries, XPath, CSS - ```clojure - (fill driver ".//input[@id='lst-ib'][@name='q']" "XPath in action!" keys/enter) - ``` +:xpath-sel: https://www.w3schools.com/xml/xpath_syntax.asp +:css-sel: https://www.w3schools.com/cssref/css_selectors.asp -- a map with either `:xpath` or `:css` key with a string expression of - corresponding syntax. Example: - - ```clojure - (fill driver {:xpath ".//input[@id='lst-ib']"} "XPath selector" keys/enter) - (fill driver {:css "input#lst-ib[name='q']"} "CSS selector" keys/enter) - ``` - - See the [CSS selector][css-sel] manual for more info. - - -### Map syntax for querying - -A query might be any other map that represents an XPath expression as data. The -rules are: +* `:active` stands for the current active element. +When opening Google page for example, it focuses the cursor on the main search input. +So there is no need to click on in manually. +Example: ++ +[source,clojure] +---- +(fill driver :active "Let's search for something" keys/enter) +---- + +* any other keyword that indicates an element's ID. +For Google page, it is `:lst-ib` or `"lst-ib"` (strings are also supported). +The registry matters. +Example: ++ +[source,clojure] +---- +(fill driver :lst-ib "What is the Matrix?" keys/enter) +---- + +* a string with an link:{xpath-sel}[XPath] expression. +Be careful when writing them manually. +Check the `Troubleshooting` section below. +Example: ++ +[source,clojure] +---- +(fill driver ".//input[@id='lst-ib'][@name='q']" "XPath in action!" keys/enter) +---- -- A `:tag` key represents a tag's name. It becomes `*` when not passed. -- An `:index` key expands into the trailing `[x]` clause. Useful when you need - to select a third row from a table for example. -- Any non-special key represents an attribute and its value. -- A special key has `:fn/` namespace and expands into something specific. +* a map with either `:xpath` or `:css` key with a string expression of corresponding syntax. +Example: ++ +[source,clojure] +---- +(fill driver {:xpath ".//input[@id='lst-ib']"} "XPath selector" keys/enter) +(fill driver {:css "input#lst-ib[name='q']"} "CSS selector" keys/enter) +---- ++ +See the link:{css-sel}[CSS selector] manual for more info. + +=== Map syntax for querying + +A query might be any other map that represents an XPath expression as data. +The rules are: + +* A `:tag` key represents a tag's name. +It becomes `*` when not passed. +* An `:index` key expands into the trailing `[x]` clause. +Useful when you need to select a third row from a table for example. +* Any non-special key represents an attribute and its value. +* A special key has `:fn/` namespace and expands into something specific. Examples: -- find the first `div` tag - ```clojure - (query driver {:tag :div}) - ;; expands into .//div - ``` - -- find the n-th `div` tag - ```clojure - (query driver {:tag :div :index 1}) - ;; expands into .//div[1] - ``` - -- find the tag `a` with the class attribute equals to `active` - -``` clojure +* find the first `div` tag ++ +[source,clojure] +---- +(query driver {:tag :div}) +;; expands into .//div +---- + +* find the n-th `div` tag ++ +[source,clojure] +---- +(query driver {:tag :div :index 1}) +;; expands into .//div[1] +---- + +* find the tag `a` with the class attribute equals to `active` ++ +[source,clojure] +---- (query driver {:tag :a :class "active"}) ;; ".//a[@class=\"active\"]" -``` - -- find a form by its attributes: - - ```clojure - (query driver {:tag :form :method :GET :class :message}) - ;; expands into .//form[@method="GET"][@class="message"] - ``` - -- find a button by its text (exact match): - - ```clojure - (query driver {:tag :button :fn/text "Press Me"}) - ;; .//button[text()="Press Me"] - ``` - -- find an nth element (`p`, `a`, whatever) with "download" text: - - ```clojure - (query driver {:fn/has-text "download" :index 3}) - ;; .//*[contains(text(), "download")][3] - ``` - -- find an element that has the following class: - - ```clojure - (query driver {:tag :div :fn/has-class "overlay"}) - ;; .//div[contains(@class, "overlay")] - ``` - -- find an element that has the following domain in a href: - - ```clojure - (query driver {:tag :a :fn/link "google.com"}) - ;; .//a[contains(@href, "google.com")] - ``` - -- find an element that has the following classes at once: - - ```clojure - (query driver {:fn/has-classes [:active :sticky :marked]}) - ;; .//*[contains(@class, "active")][contains(@class, "sticky")][contains(@class, "marked")] - ``` - -- find the enabled/disabled input widgets: - - ```clojure - ;; 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()] - ``` - -### Vector syntax for querying - -A query might be a vector that consists from any expressions mentioned above. In -such a query, every next term searches from a previous one recursively. +---- + +* find a form by its attributes: ++ +[source,clojure] +---- +(query driver {:tag :form :method :GET :class :message}) +;; expands into .//form[@method="GET"][@class="message"] +---- + +* find a button by its text (exact match): ++ +[source,clojure] +---- +(query driver {:tag :button :fn/text "Press Me"}) +;; .//button[text()="Press Me"] +---- + +* find an nth element (`p`, `a`, whatever) with "download" text: ++ +[source,clojure] +---- +(query driver {:fn/has-text "download" :index 3}) +;; .//*[contains(text(), "download")][3] +---- + +* find an element that has the following class: ++ +[source,clojure] +---- +(query driver {:tag :div :fn/has-class "overlay"}) +;; .//div[contains(@class, "overlay")] +---- + +* find an element that has the following domain in a href: ++ +[source,clojure] +---- +(query driver {:tag :a :fn/link "google.com"}) +;; .//a[contains(@href, "google.com")] +---- + +* find an element that has the following classes at once: ++ +[source,clojure] +---- +(query driver {:fn/has-classes [:active :sticky :marked]}) +;; .//*[contains(@class, "active")][contains(@class, "sticky")][contains(@class, "marked")] +---- + +* find the enabled/disabled input widgets: ++ +[source,clojure] +---- +;; 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()] +---- + +=== Vector syntax for querying + +A query might be a vector that consists from any expressions mentioned above. +In such a query, every next term searches from a previous one recursively. A simple example: -```clojure +[source,clojure] +---- (click driver [{:tag :html} {:tag :body} {:tag :a}]) -``` +---- -You may combine both XPath and CSS expressions as well (pay attention at a -leading dot in XPath expression: +You may combine both XPath and CSS expressions as well (pay attention at a leading dot in XPath expression: -```clojure +[source,clojure] +---- (click driver [{:tag :html} {:css "div.class"} ".//a[@class='download']"]) -``` +---- -### Advanced queries +=== Advanced queries -#### Querying the *nth* element matched +==== Querying the _nth_ element matched -Sometimes you may need to interact with the *nth* element of a query, for -instance when wanting to click on the second link in this example: +Sometimes you may need to interact with the _nth_ element of a query, for instance when wanting to click on the second link in this example: -```html +[source,html] +----
  • a @@ -496,156 +449,161 @@ instance when wanting to click on the second link in this example: c
-``` +---- -In this case you may either use the `:index` directive that is supported for -XPath expressions like this: +In this case you may either use the `:index` directive that is supported for XPath expressions like this: -```clojure +[source,clojure] +---- (click driver [{:tag :li :class :search-result :index 2} {:tag :a}]) -``` +---- +:nth-child: https://www.w3schools.com/CSSref/sel_nth-child.asp -[nth-child]: https://www.w3schools.com/CSSref/sel_nth-child.asp +or you can use the link:{nth-child}[nth-child trick] with the CSS expression like this: -or you can use the [nth-child trick][nth-child] with the CSS expression like -this: - -```clojure +[source,clojure] +---- (click driver {:css "li.search-result:nth-child(2) a"}) -``` +---- -Finally it is also possible to obtain the *nth* element directly by using -`query-all`: +Finally it is also possible to obtain the _nth_ element directly by using `query-all`: -```clojure +[source,clojure] +---- (click-el driver (nth (query-all driver {:css "li.search-result a"}) 2)) -``` +---- -Note the use of `click-el` here, as `query-all` returns an element, not a -selector that can be passed to `click` directly. +Note the use of `click-el` here, as `query-all` returns an element, not a selector that can be passed to `click` directly. -#### Getting elements like in a tree +==== Getting elements like in a tree `query-tree` takes selectors and acts like a tree. Every next selector queries elements from the previous ones. The fist selector relies on find-elements, and the rest ones use find-elements-from - ```clojure +[source,clojure] +---- (query-tree driver {:tag :div} {:tag :a}) - ``` +---- - means +means - ``` +[source,clojure] +---- {:tag :div} -> [div1 div2 div3] div1 -> [a1 a2 a3] div2 -> [a4 a5 a6] div3 -> [a7 a8 a9] - ``` - so the result will be [a1 ... a9] +---- + +so the result will be `[a1 ... a9]` -### Interacting with queried elements +=== Interacting with queried elements -To interact with elements found via a query you have to pass the query result to -either `click-el` or `fill-el`: +To interact with elements found via a query you have to pass the query result to either `click-el` or `fill-el`: -```clojure +[source,clojure] +---- (click-el driver (first (query-all driver {:tag :a}))) -``` +---- -So you may collect elements into a vector and arbitrarily interact with them -at any time: +So you may collect elements into a vector and arbitrarily interact with them at any time: -```clojure +[source,clojure] +---- (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!") -``` +---- - -## Emulation of human input +== Emulation of human input For the purpose of emulating human input, you can use the `fill-human` function. The following options are enabled by default: -``` clojure +[source,clojure] +---- {: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 -``` +---- and you can redefine them: -``` clojure +[source,clojure] +---- (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) -``` +---- for multiple input with human emulation, use `fill-human-multi` -``` clojure +[source,clojure] +---- (fill-human-multi driver {:login "login" :pass "password" :textarea "some text"} {:mistake-prob 0.5 :pause-max 1}) -``` +---- -## Mouse clicks +== Mouse clicks -The `click` function triggers the left mouse click on an element found by a -query term: +The `click` function triggers the left mouse click on an element found by a query term: -```clojure +[source,clojure] +---- (click driver {:tag :button}) -``` +---- -The `click` function uses only the first element found by the query, which -sometimes leads to clicking on the wrong items. To ensure there is one and only -one element found, use the `click-single` function. It acts the same but raises -an exception when querying the page returns multiple elements: +The `click` function uses only the first element found by the query, which sometimes leads to clicking on the wrong items. +To ensure there is one and only one element found, use the `click-single` function. +It acts the same but raises an exception when querying the page returns multiple elements: -```clojure +[source,clojure] +---- (click-single driver {:tag :button :name "search"}) -``` +---- -A double click is used rarely in web yet is possible with the `double-click` -function (Chrome, Phantom.js): +A double click is used rarely in web yet is possible with the `double-click` function (Chrome, Phantom.js): -```clojure +[source,clojure] +---- (double-click driver {:tag :dbl-click-btn}) -``` +---- -There is also a bunch of "blind" clicking functions. They trigger mouse clicks -on the current mouse position: +There is also a bunch of "blind" clicking functions. +They trigger mouse clicks on the current mouse position: -```clojure +[source,clojure] +---- (left-click driver) (middle-click driver) (right-click driver) -``` +---- -Another bunch of functions do the same but move the mouse pointer to a specified -element before clicking on them: +Another bunch of functions do the same but move the mouse pointer to a specified element before clicking on them: -```clojure +[source,clojure] +---- (left-click-on driver {:tag :img}) (middle-click-on driver {:tag :img}) (right-click-on driver {:tag :img}) -``` +---- -A middle mouse click is useful when opening a link in a new background tab. The -right click sometimes is used to imitate a context menu in web applications. +A middle mouse click is useful when opening a link in a new background tab. +The right click sometimes is used to imitate a context menu in web applications. -## Actions +== Actions -The library supports [Webdriver Actions][actions]. In general, actions are -commands describing virtual input devices. +The library supports link:{actions}[Webdriver Actions]. +In general, actions are commands describing virtual input devices. -``` clojure +[source,clojure] +---- {:actions [{:type "key" :id "some name" :actions [{:type "keyDown" :value cmd} @@ -662,11 +620,12 @@ commands describing virtual input devices. {:type "pointerUp" :duration 0 :button 0} {:type "pointerDown" :duration 0 :button 0} {:type "pointerUp" :duration 0 :button 0}]}]} -``` +---- You can create a map manually and send it to the `perform-actions` method: -``` clojure +[source,clojure] +---- (def keyboard-input {:type "key" :id "some name" :actions [{:type "keyDown" :value cmd} @@ -676,27 +635,31 @@ You can create a map manually and send it to the `perform-actions` method: {:type "pause" :duration 100}]}) (perform-actions driver keyboard-input) -``` +---- -or use wrappers. First you need to create a virtual input devices, for example: +or use wrappers. +First you need to create a virtual input devices, for example: -``` clojure +[source,clojure] +---- (def keyboard (make-key-input)) -``` +---- and then fill it with the necessary actions: -``` clojure +[source,clojure] +---- (-> keyboard (add-key-down keys/shift-left) (add-key-down "a") (add-key-up "a") (add-key-up keys/shift-left)) -``` +---- extended example: -``` clojure +[source,clojure] +---- (let [driver (chrome) _ (go driver "https://google.com") search-box (query driver {:name :q}) @@ -714,24 +677,25 @@ extended example: (add-key-press keys/enter))] (perform-actions driver keyboard mouse) (quit driver)) -``` +---- To clear the state of virtual input devices, release all pressed keys etc, use the `release-actions` method: -``` clojure +[source,clojure] +---- (release-actions driver) -``` +---- -## File uploading +== File uploading -Clicking on a file input button opens an OS-specific dialog that you are not -allowed to interact with using WebDriver protocol. Use the `upload-file` -function to attach a local file to a file input widget. The function takes a -selector that points to a file input and either a full path as a string or a -native `java.io.File` instance. The file should exist or you'll get an exception -otherwise. Usage example: +Clicking on a file input button opens an OS-specific dialog that you are not allowed to interact with using WebDriver protocol. +Use the `upload-file` function to attach a local file to a file input widget. +The function takes a selector that points to a file input and either a full path as a string or a native `java.io.File` instance. +The file should exist or you'll get an exception otherwise. +Usage example: -```clojure +[source,clojure] +---- (def driver (chrome)) ;; open a web page that serves uploaded files @@ -748,97 +712,102 @@ otherwise. Usage example: (require '[clojure.java.io :as io]) (def my-file (io/file "/Users/ivan/Downloads/sample.png")) (upload-file driver input my-file) -``` +---- -## Screenshots +== Screenshots -Calling a `screenshot` function dumps the current page into a PNG image on your -disk: +Calling a `screenshot` function dumps the current page into a PNG image on your disk: -```clojure +[source,clojure] +---- (screenshot driver "page.png") ;; relative path (screenshot driver "/Users/ivan/page.png") ;; absolute path -``` +---- A native Java File object is also supported: -```clojure +[source,clojure] +---- ;; when imported as `[clojure.java.io :as io]` (screenshot driver (io/file "test.png")) ;; native object (screenshot driver (java.io.File. "test-native.png")) -``` +---- -### Screening elements +=== Screening elements -With Firefox and Chrome, you may capture not the whole page but a single element, -say a div, an input widget or whatever. It doesn't work with other browsers for -now. Example: +With Firefox and Chrome, you may capture not the whole page but a single element, say a div, an input widget or whatever. +It doesn't work with other browsers for now. +Example: -```clojure +[source,clojure] +---- (screenshot-element driver {:tag :div :class :smart-widget} "smart_widget.png") -``` +---- -### Screening after each form +=== Screening after each form With macro `with-screenshots`, you can make a screenshot after each form -``` clojure +[source,clojure] +---- (with-screenshots driver "../screenshots" (fill driver :simple-input "1") (fill driver :simple-input "2") (fill driver :simple-input "3")) -``` +---- what is equivalent to a record: -``` clojure +[source,clojure] +---- (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") -``` +---- -## Using headless drivers +== Using headless drivers -Recently, Google Chrome and later Firefox started support a feature named -headless mode. When being headless, none of UI windows occur on the screen, only -the stdout output goes into console. This feature allows you to run integration -tests on servers that do not have graphical output device. +Recently, Google Chrome and later Firefox started support a feature named headless mode. +When being headless, none of UI windows occur on the screen, only the stdout output goes into console. +This feature allows you to run integration tests on servers that do not have graphical output device. -Ensure your browser supports headless mode by checking if it accepts `--headles` -command line argument when running it from the terminal. Phantom.js driver is -headless by its nature (it has never been developed for rendering UI). +Ensure your browser supports headless mode by checking if it accepts `--headles` command line argument when running it from the terminal. +Phantom.js driver is headless by its nature (it has never been developed for rendering UI). -When starting a driver, pass `:headless` boolean flag to switch into headless -mode. Note, only latest version of Chrome and Firefox are supported. For other -drivers, the flag will be ignored. +When starting a driver, pass `:headless` boolean flag to switch into headless mode. +Note, only latest version of Chrome and Firefox are supported. +For other drivers, the flag will be ignored. -```clojure +[source,clojure] +---- (def driver (chrome {:headless true})) ;; runs headless Chrome -``` +---- or -```clojure +[source,clojure] +---- (def driver (firefox {:headless true})) ;; runs headless Firefox -``` +---- To check of any driver has been run in headless mode, use `headless?` predicate: -```clojure +[source,clojure] +---- (headless? driver) ;; true -``` +---- Note, it will always return true for Phantom.js instances. -There are several shortcuts to run Chrome or Firefox in headless mode by -default: +There are several shortcuts to run Chrome or Firefox in headless mode by default: -```clojure +[source,clojure] +---- (def driver (chrome-headless)) ;; or @@ -852,65 +821,66 @@ default: (with-firefox-headless {...} driver ;; extra settings (go driver "http://example.com")) -``` +---- -There are also `when-headless` and `when-not-headless` macroses that allow to -perform a bunch of commands only if a browser is in headless mode or not -respectively: +There are also `when-headless` and `when-not-headless` macroses that allow to perform a bunch of commands only if a browser is in headless mode or not respectively: -```clojure +[source,clojure] +---- (with-chrome nil driver ... (when-not-headless driver ... some actions that might be not available in headless mode) ... common actions for both versions) -``` +---- -## Connection to remote webdriver +== Connection to remote webdriver -To connect to a driver already running on a local or remote host, you must specify the `:host` parameter -which might be either a hostname (localhost, some.remote.host.net) or an IP address (127.0.0.1, 183.102.156.31) and the `:port`. +To connect to a driver already running on a local or remote host, you must specify the `:host` parameter which might be either a hostname (localhost, some.remote.host.net) or an IP address (127.0.0.1, 183.102.156.31) and the `:port`. If the port is not specified, the default port is set. Example: -```clojure +[source,clojure] +---- ;; 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 -``` +---- -## Webdriver in Docker +== Webdriver in Docker To work with the driver in Docker, you can take ready-made images: -Example for [Chrome](https://hub.docker.com/r/robcherry/docker-chromedriver/): +Example for https://hub.docker.com/r/robcherry/docker-chromedriver/[Chrome]: -``` +---- docker run --name chromedriver -p 9515:4444 -d -e CHROMEDRIVER_WHITELISTED_IPS='' robcherry/docker-chromedriver:latest -``` +---- -for [Firefox](https://hub.docker.com/r/instrumentisto/geckodriver): +for https://hub.docker.com/r/instrumentisto/geckodriver[Firefox]: -``` +---- docker run --name geckodriver -p 4444:4444 -d instrumentisto/geckodriver -``` +---- To connect to the driver you just need to specify the `:host` parameter as `localhost` or `127.0.0.1` and the `:port` on which it is running. If the port is not specified, the default port is set. -``` clojure +[source,clojure] +---- (def driver (chrome-headless {:host "localhost" :port 9515 :args ["--no-sandbox"]})) (def driver (firefox-headless {:host "localhost"})) ;; will try to connect to port 4444 -``` +---- -## HTTP Proxy +== HTTP Proxy To set proxy settings use environment variables `HTTP_PROXY`/`HTTPS_PROXY` or pass a map of the following type: -``` clojure +[source,clojure] +---- {:proxy {:http "some.proxy.com:8080" :ftp "some.proxy.com:8080" :ssl "some.proxy.com:8080" @@ -921,13 +891,15 @@ To set proxy settings use environment variables `HTTP_PROXY`/`HTTPS_PROXY` or pa ;; example (chrome {:proxy {:http "some.proxy.com:8080" :ssl "some.proxy.com:8080"}}) -``` -Note: A :pac-url for a [proxy autoconfiguration file](https://en.wikipedia.org/wiki/Proxy_auto-config#The_PAC_File). +---- + +NOTE: A :pac-url for a https://en.wikipedia.org/wiki/Proxy_auto-config#The_PAC_File[proxy autoconfiguration file]. Used with Safari as the other proxy options do not work in that browser. -To fine tune the proxy you can use the original [object](https://www.w3.org/TR/webdriver/#proxy) and pass it to capabilities: +To fine tune the proxy you can use the original https://www.w3.org/TR/webdriver/#proxy[object] and pass it to capabilities: -``` clojure +[source,clojure] +---- {:capabilities {:proxy {:proxyType "manual" :proxyAutoconfigUrl "some.proxy.com:8080" :ftpProxy "some.proxy.com:8080" @@ -938,27 +910,28 @@ To fine tune the proxy you can use the original [object](https://www.w3.org/TR/w :socksVersion 5}}} (chrome {:capabilities {:proxy {...}}}) -``` +---- -## Devtools: tracking HTTP requests, XHR (Ajax) +== Devtools: tracking HTTP requests, XHR (Ajax) -With recent updates, the library brings a great feature. Now you can trace -events which come from the DevTools panel. It means, everything you see in the -developer console now is available through API. That works only with Google -Chrome now. +With recent updates, the library brings a great feature. +Now you can trace events which come from the DevTools panel. +It means, everything you see in the developer console now is available through API. +That works only with Google Chrome now. -To start a driver with special development settings specified, just pass an -empty map to the `:dev` field when running a driver: +To start a driver with special development settings specified, just pass an empty map to the `:dev` field when running a driver: -```clojure +[source,clojure] +---- (def c (chrome {:dev {}})) -``` +---- -The value must not be `nil`. When it's an empty map, a special function takes -defaults. Here is a full version of dev settings with all the possible values -specified. +The value must not be `nil`. +When it's an empty map, a special function takes defaults. +Here is a full version of dev settings with all the possible values specified. -```clojure +[source,clojure] +---- (def c (chrome {:dev {:perf {:level :all @@ -968,25 +941,25 @@ specified. :categories [:devtools :devtools.network :devtools.timeline]}}})) -``` +---- -Under the hood, it fills a special `perfLoggingPrefs` dictionary inside the -`chromeOptions` object. +Under the hood, it fills a special `perfLoggingPrefs` dictionary inside the `chromeOptions` object. -Now that your browser accumulates these events, you can read them using a -special `dev` namespace. +Now that your browser accumulates these events, you can read them using a special `dev` namespace. -```clojure +[source,clojure] +---- (go c "http://google.com") ;; wait until the page gets loaded ;; load the namespace (require '[etaoin.dev :as dev]) -``` +---- Let's have a list of ALL the HTTP requests happened during the page was loading. -```clojure +[source,clojure] +---- (def reqs (dev/get-requests c)) ;; reqs is a vector of maps @@ -997,9 +970,10 @@ Let's have a list of ALL the HTTP requests happened during the page was loading. (set (map :type reqs)) ;; #{:script :other :document :image :xhr} ;; we've got Js requests, images, AJAX and other stuff -``` +---- -```clojure +[source,clojure] +---- ;; check the last one request, it's an image named tia.png (-> reqs last clojure.pprint/pprint) @@ -1021,12 +995,12 @@ Let's have a list of ALL the HTTP requests happened during the page was loading. :mime "image/png", :remote-ip "173.194.73.94"}, :done? true} -``` +---- -Since we're mostly interested in AJAX requests, there is a function `get-ajax` -that does the same but filters XHR requests: +Since we're mostly interested in AJAX requests, there is a function `get-ajax` that does the same but filters XHR requests: -```clojure +[source,clojure] +---- (-> c dev/get-ajax last clojure.pprint/pprint) {:state 4, @@ -1048,20 +1022,21 @@ that does the same but filters XHR requests: :mime "application/json", :remote-ip "74.125.131.99"}, :done? true} -``` +---- -A typical pattern of `get-ajax` usage is the following. You'd like to check if a -certain request has been fired to the server. So you press a button, wait for a -while and then read the requests made by your browser. +A typical pattern of `get-ajax` usage is the following. +You'd like to check if a certain request has been fired to the server. +So you press a button, wait for a while and then read the requests made by your browser. -Having a list of requests, you search for the one you need (e.g. by its URL) and -then check its state. The `:state` field's got the same semantics like the -`XMLHttpRequest.readyState` has. It's an integer from 1 to 4 with the same -behavior. +Having a list of requests, you search for the one you need (e.g. +by its URL) and then check its state. +The `:state` field's got the same semantics like the `XMLHttpRequest.readyState` has. +It's an integer from 1 to 4 with the same behavior. To check if a request has been finished, done or failed, use these predicates: -```clojure +[source,clojure] +---- (def req (last reqs)) (dev/request-done? req) @@ -1072,20 +1047,19 @@ To check if a request has been finished, done or failed, use these predicates: (dev/request-success? req) ;; true -``` +---- -Note that `request-done?` doesn't mean the request has succeeded. It only means -its pipeline has reached a final step. +Note that `request-done?` doesn't mean the request has succeeded. +It only means its pipeline has reached a final step. -**Warning:** when you read dev logs, you consume them from an internal buffer -which gets flushed. The second call to `get-requests` or `get-ajax` will return -an empty list. +WARNING: when you read dev logs, you consume them from an internal buffer which gets flushed. +The second call to `get-requests` or `get-ajax` will return an empty list. -Perhaps you want to collect these logs by your own. A function -`dev/get-performance-logs` return a list of logs so you accumulate them in an -atom or whatever: +Perhaps you want to collect these logs by your own. +A function `dev/get-performance-logs` return a list of logs so you accumulate them in an atom or whatever: -```clojure +[source,clojure] +---- (def logs (atom [])) ;; repeat that form from time to time @@ -1094,50 +1068,49 @@ atom or whatever: (count @logs) ;; 76 -``` +---- -There are `logs->requests` and `logs->ajax` functions that convert logs into -requests. Unlike `get-requests` and `get-ajax`, they are pure functions and won't -flush anything. +There are `+logs->requests+` and `+logs->ajax+` functions that convert logs into requests. +Unlike `get-requests` and `get-ajax`, they are pure functions and won't flush anything. -```clojure +[source,clojure] +---- (dev/logs->requests @logs) -``` +---- -When working with logs and requests, pay attention it their count and size. The -maps have got plenty of keys and the amount of items in collections might be -huge. Printing a whole bunch of events might freeze your editor. Consider using -`clojure.pprint/pprint` function as it relies on max level and length limits. +When working with logs and requests, pay attention it their count and size. +The maps have got plenty of keys and the amount of items in collections might be huge. +Printing a whole bunch of events might freeze your editor. +Consider using `clojure.pprint/pprint` function as it relies on max level and length limits. -## Postmortem: auto-save artifacts in case of exception +== Postmortem: auto-save artifacts in case of exception -Sometimes, it might be difficult to discover what went wrong during the last UI -tests session. A special macro `with-postmortem` saves some useful data on disk -before the exception was triggered. Those data are a screenshot, HTML code and -JS console logs. Note: not all browsers support getting JS logs. +Sometimes, it might be difficult to discover what went wrong during the last UI tests session. +A special macro `with-postmortem` saves some useful data on disk before the exception was triggered. +Those data are a screenshot, HTML code and JS console logs. +Note: not all browsers support getting JS logs. Example: -```clojure +[source,clojure] +---- (def driver (chrome)) (with-postmortem driver {:dir "/Users/ivan/artifacts"} (click driver :non-existing-element)) -``` +---- -An exception will rise, but in `/Users/ivan/artifacts` there will be three files -named by a template `---.`: +An exception will rise, but in `/Users/ivan/artifacts` there will be three files named by a template `---.`: -- `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`: the current browser's HTML - content; -- `firefox-127.0.0.1-4444-2017-03-26-02-45-07.json`: a JSON file with console - logs; those are a vector of objects. +* `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`: the current browser's HTML content; +* `firefox-127.0.0.1-4444-2017-03-26-02-45-07.json`: a JSON file with console logs; +those are a vector of objects. -The handler takes a map of options with the following keys. All of them might be -absent. +The handler takes a map of options with the following keys. +All of them might be absent. -```clojure +[source,clojure] +---- {;; default directory where to store artifacts ;; might not exist, will be created otherwise. pwd is used when not passed :dir "/home/ivan/UI-tests" @@ -1153,40 +1126,42 @@ absent. ;; a string template to format a date; See SimpleDateFormat Java class :date-format "yyyy-MM-dd-HH-mm-ss"} -``` +---- -## Reading browser's logs +== Reading browser's logs -Function `(get-logs driver)` returns the browser's logs as a vector of -maps. Each map has the following structure: +Function `(get-logs driver)` returns the browser's logs as a vector of maps. +Each map has the following structure: -```clojure +[source,clojure] +---- {:level :warning, :message "1,2,3,4 anonymous (:1)", :timestamp 1511449388366, :source nil, :datetime #inst "2017-11-23T15:03:08.366-00:00"} -``` +---- -Currently, logs are available in Chrome and Phantom.js only. Please note, the -message text and the source type highly depend on the browser. Chrome wipes the -logs once they have been read. Phantom.js keeps them but only until you change -the page. +Currently, logs are available in Chrome and Phantom.js only. +Please note, the message text and the source type highly depend on the browser. +Chrome wipes the logs once they have been read. +Phantom.js keeps them but only until you change the page. -## Additional parameters +== Additional parameters -When running a driver instance, a map of additional parameters might be passed -to tweak the browser's behaviour: +When running a driver instance, a map of additional parameters might be passed to tweak the browser's behaviour: -```clojure +[source,clojure] +---- (def driver (chrome {:path "/path/to/driver/binary"})) -``` +---- -Below, here is a map of parameters the library support. All of them might be -skipped or have nil values. Some of them, if not passed, are taken from the -`defaults` map. +Below, here is a map of parameters the library support. +All of them might be skipped or have nil values. +Some of them, if not passed, are taken from the `defaults` map. -```clojure +[source,clojure] +---- {;; 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. @@ -1246,177 +1221,172 @@ skipped or have nil values. Some of them, if not passed, are taken from the ;; Driver-specific options. Make sure you have read the docs before setting them. :capabilities {:chromeOptions {:args ["--headless"]}}} -``` +---- -## Eager page load +== Eager page load -When you navigate to a certain page, the driver waits until the whole page has -been completely loaded. What's fine in most of the cases yet doesn't reflect the -way human beings interact with the Internet. +When you navigate to a certain page, the driver waits until the whole page has been completely loaded. +What's fine in most of the cases yet doesn't reflect the way human beings interact with the Internet. -Change this default behavior with the `:load-strategy` option. There are three -possible values for that: `:none`, `:eager` and `:normal` which is the default -when not passed. +Change this default behavior with the `:load-strategy` option. +There are three possible values for that: `:none`, `:eager` and `:normal` which is the default when not passed. -When you pass `:none`, the driver responds immediately so you are welcome to -execute next instructions. For example: +When you pass `:none`, the driver responds immediately so you are welcome to execute next instructions. +For example: -```clojure +[source,clojure] +---- (def c (chrome)) (go c "http://some.slow.site.com") ;; you'll hang on this line until the page loads (do-something) -``` +---- Now when passing the load strategy option: -```clojure +[source,clojure] +---- (def c (chrome {:load-strategy :none})) (go c "http://some.slow.site.com") ;; no pause, acts immediately (do-something) -``` - -For the `:eager` option, it works only with Firefox at the moment of adding the -feature to the library. +---- +For the `:eager` option, it works only with Firefox at the moment of adding the feature to the library. -## Keyboard chords +== Keyboard chords -There is an option to input a series of keys simultaneously. That is useful to -imitate holding a system key like Control, Shift or whatever when typing. +There is an option to input a series of keys simultaneously. +That is useful to imitate holding a system key like Control, Shift or whatever when typing. -The namespace `etaoin.keys` carries a bunch of key constants as well as a set of -functions related to input. +The namespace `etaoin.keys` carries a bunch of key constants as well as a set of functions related to input. -```clojure +[source,clojure] +---- (require '[etaoin.keys :as keys]) -``` +---- A quick example of entering ordinary characters holding Shift: -```clojure +[source,clojure] +---- (def c (chrome)) (go c "http://google.com") (fill-active c (keys/with-shift "caps is great")) -``` +---- -The main input gets populated with "CAPS IS GREAT". Now you'd like to delete the -last word. In Chrome, this is done by pressing backspace holding Alt. Let's do -that: +The main input gets populated with "CAPS IS GREAT". +Now you'd like to delete the last word. +In Chrome, this is done by pressing backspace holding Alt. +Let's do that: -```clojure +[source,clojure] +---- (fill-active c (keys/with-alt keys/backspace)) -``` +---- Now you've got only "CAPS IS " in the input. -Consider a more complex example which repeats real users' behaviour. You'd like -to delete everything from the input. First, you move the caret at the very -beginning. Then move it to the end holding shift so everything gets -selected. Finally, you press delete to clear the selected text. +Consider a more complex example which repeats real users' behaviour. +You'd like to delete everything from the input. +First, you move the caret at the very beginning. +Then move it to the end holding shift so everything gets selected. +Finally, you press delete to clear the selected text. The combo is: -```clojure +[source,clojure] +---- (fill-active c keys/home (keys/with-shift keys/end) keys/delete) -``` +---- There are also `with-ctrl` and `with-command` functions that act the same. -Pay attention, these functions do not apply to the global browser's -shortcuts. For example, neither "Command + R" nor "Command + T" reload the page -or open a new tab. +Pay attention, these functions do not apply to the global browser's shortcuts. +For example, neither "Command + R" nor "Command + T" reload the page or open a new tab. -All the `keys/with-*` functions are just wrappers upon the `keys/chord` function -that might be used for complex cases. +All the `keys/with-*` functions are just wrappers upon the `keys/chord` function that might be used for complex cases. +== File download directory -## File download directory +To specify your own directory where to download files, pass `:download-dir` parameter into an option map when running a driver: -To specify your own directory where to download files, pass `:download-dir` -parameter into an option map when running a driver: - -```clojure +[source,clojure] +---- (def driver (chrome {:download-dir "/Users/ivan/Desktop"})) -``` +---- -Now, once you click on a link, a file should be put into that folder. Currently, -only Chrome and Firefox are supported. +Now, once you click on a link, a file should be put into that folder. +Currently, only Chrome and Firefox are supported. -Firefox requires to specify MIME-types of those files that should be downloaded -without showing a system dialog. By default, when the `:download-dir` parameter -is passed, the library adds the most common MIME-types: archives, media files, -office documents, etc. If you need to add your own one, override that preference -manually: +Firefox requires to specify MIME-types of those files that should be downloaded without showing a system dialog. +By default, when the `:download-dir` parameter is passed, the library adds the most common MIME-types: archives, media files, office documents, etc. +If you need to add your own one, override that preference manually: -```clojure +[source,clojure] +---- (def driver (firefox {:download-dir "/Users/ivan/Desktop" :prefs {:browser.helperApps.neverAsk.saveToDisk "some-mime/type-1;other-mime/type-2"}})) -``` +---- -To check whether a file was downloaded during UI tests, see the testing section -below. +To check whether a file was downloaded during UI tests, see the testing section below. -## Managing User-Agent +== Managing User-Agent -Set a custom User-Agent header with the `:user-agent` option when creating a -driver, for example: +Set a custom User-Agent header with the `:user-agent` option when creating a driver, for example: -~~~clojure +[source,clojure] +---- (def f (firefox {:user-agent "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"})) -~~~ +---- To get the current value of the header in runtime, use the function: -~~~clojure +[source,clojure] +---- (get-user-agent f) ;; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) -~~~ - -Setting that header is quite important for headless browsers as most of the -sites check if the User-Agent includes the "headless" string. This could lead to -403 response or some weird behavior of the site. +---- -## Setting browser profile +Setting that header is quite important for headless browsers as most of the sites check if the User-Agent includes the "headless" string. +This could lead to 403 response or some weird behavior of the site. -When running Chrome or Firefox, you may specify a special profile made for test -purposes. A profile is a folder that keeps browser settings, history, bookmarks -and other user-specific data. +== Setting browser profile -Imagine you'd like to run your integration tests against a user that turned off -Javascript execution or image rendering. To prepare a special profile for that -task would be a good choice. +When running Chrome or Firefox, you may specify a special profile made for test purposes. +A profile is a folder that keeps browser settings, history, bookmarks and other user-specific data. -### Create and find a profile in Chrome +Imagine you'd like to run your integration tests against a user that turned off Javascript execution or image rendering. +To prepare a special profile for that task would be a good choice. -1. In the right top corner of the main window, click on a user button. -2. In the dropdown, select "Manage People". -3. Click "Add person", submit a name and press "Save". -4. The new browser window should appear. Now, setup the new profile as you want. -5. Open `chrome://version/` page. Copy the file path that is beneath the - `Profile Path` caption. +=== Create and find a profile in Chrome -### Create and find a profile in Firefox +. In the right top corner of the main window, click on a user button. +. In the dropdown, select "Manage People". +. Click "Add person", submit a name and press "Save". +. The new browser window should appear. +Now, setup the new profile as you want. +. Open `chrome://version/` page. +Copy the file path that is beneath the `Profile Path` caption. -[ff-profile]:https://support.mozilla.org/en-US/kb/profile-manager-create-and-remove-firefox-profiles +=== Create and find a profile in Firefox -1. Run Firefox with `-P`, `-p` or `-ProfileManager` key as the [official - page][ff-profile] describes. -2. Create a new profile and run the browser. -3. Setup the profile as you need. -4. Open `about:support` page. Near the `Profile Folder` caption, press the `Show - in Finder` button. A new folder window should appear. Copy its path from - there. +. Run Firefox with `-P`, `-p` or `-ProfileManager` key as the https://support.mozilla.org/en-US/kb/profile-manager-create-and-remove-firefox-profiles[official page] describes. +. Create a new profile and run the browser. +. Setup the profile as you need. +. Open `about:support` page. +Near the `Profile Folder` caption, press the `Show in Finder` button. +A new folder window should appear. +Copy its path from there. -### Running a driver with a profile +=== Running a driver with a profile -Once you've got a profile path, launch a driver with a special `:profile` key as -follows: +Once you've got a profile path, launch a driver with a special `:profile` key as follows: -```clojure +[source,clojure] +---- ;; Chrome (def chrome-profile "/Users/ivan/Library/Application Support/Google/Chrome/Profile 2/Default") @@ -1428,16 +1398,16 @@ follows: "/Users/ivan/Library/Application Support/Firefox/Profiles/iy4iitbg.Test") (def ff (firefox {:profile ff-profile})) -``` +---- -## Scrolling +== Scrolling The library ships a set of functions to scroll the page. -The most important one, `scroll-query` jumps the the first element found with -the query term: +The most important one, `scroll-query` jumps the the first element found with the query term: -```clojure +[source,clojure] +---- (def driver (chrome)) ;; the form button placed somewhere below @@ -1445,34 +1415,38 @@ the query term: ;; the main article (scroll-query driver {:tag :h1}) -``` +---- To jump to the absolute position, just use `scroll` as follows: -```clojure +[source,clojure] +---- (scroll driver 100 600) ;; or pass a map with x and y keys (scroll driver {:x 100 :y 600}) -``` +---- To scroll relatively, use `scroll-by` with offset values: -```clojure +[source,clojure] +---- ;; keeps the same horizontal position, goes up for 100 pixels (scroll-by driver 0 -100) ;; map parameter is also supported -``` +---- There are two shortcuts to jump top or bottom of the page: -```clojure +[source,clojure] +---- (scroll-bottom driver) ;; you'll see the footer... (scroll-top driver) ;; ...and the header again -``` +---- The following functions scroll the page in all directions: -```clojure +[source,clojure] +---- (scroll-down driver 200) ;; scrolls down by 200 pixels (scroll-down driver) ;; scrolls down by the default (100) number of pixels @@ -1484,94 +1458,97 @@ The following functions scroll the page in all directions: (scroll-right driver 200) ;; ... and right (scroll-right driver) -``` +---- -One note, in all cases the scroll actions are served with Javascript. Ensure -your browser has it enabled. +One note, in all cases the scroll actions are served with Javascript. +Ensure your browser has it enabled. -## Working with frames and iframes +== Working with frames and iframes -While working with the page, you cannot interact with those items that are put -into a frame or an iframe. The functions below switch the current context on -specific frame: +While working with the page, you cannot interact with those items that are put into a frame or an iframe. +The functions below switch the current context on specific frame: -```clojure +[source,clojure] +---- (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 -``` +---- -Frames could be nested one into another. The functions take that into -account. Say you have an HTML layout like this: +Frames could be nested one into another. +The functions take that into account. +Say you have an HTML layout like this: -```html +[source,html] +----